From 83c2c283fd84a57f475f51385ebe6e189de630ae Mon Sep 17 00:00:00 2001 From: Eli Barzilay Date: Sat, 15 May 2010 10:45:15 -0400 Subject: [PATCH] existing version of build scripts --- collects/meta/build/build | 2094 ++++++++++++++++ collects/meta/build/bundle | 562 +++++ collects/meta/build/info.rkt | 2 + collects/meta/build/make-patch | 200 ++ collects/meta/build/nsis/plt-header-r.bmp | Bin 0 -> 25818 bytes collects/meta/build/nsis/plt-header.bmp | Bin 0 -> 25818 bytes collects/meta/build/nsis/plt-installer.ico | Bin 0 -> 25214 bytes collects/meta/build/nsis/plt-installer.nsi | 305 +++ collects/meta/build/nsis/plt-uninstaller.ico | Bin 0 -> 25214 bytes collects/meta/build/nsis/plt-welcome.bmp | Bin 0 -> 154542 bytes collects/meta/build/patch-html | 93 + collects/meta/build/readme-specs | 137 + collects/meta/build/sitemap/AUTHORS | 1 + collects/meta/build/sitemap/COPYING | 37 + collects/meta/build/sitemap/ChangeLog | 65 + collects/meta/build/sitemap/PKG-INFO | 10 + collects/meta/build/sitemap/README | 25 + .../meta/build/sitemap/example_config.xml | 164 ++ .../meta/build/sitemap/example_urllist.txt | 21 + collects/meta/build/sitemap/plt-pre.xml | 16 + collects/meta/build/sitemap/setup.py | 12 + collects/meta/build/sitemap/sitemap_gen.py | 2205 +++++++++++++++++ .../meta/build/sitemap/test_sitemap_gen.py | 765 ++++++ collects/meta/build/test-drscheme.ss | 76 + .../build/unix-installer/check-install-paths | 61 + .../unix-installer/paths-configure-snapshot | 109 + .../build/unix-installer/plt-installer-header | 485 ++++ collects/meta/build/versionpatch | 104 + 28 files changed, 7549 insertions(+) create mode 100755 collects/meta/build/build create mode 100755 collects/meta/build/bundle create mode 100644 collects/meta/build/info.rkt create mode 100755 collects/meta/build/make-patch create mode 100644 collects/meta/build/nsis/plt-header-r.bmp create mode 100644 collects/meta/build/nsis/plt-header.bmp create mode 100644 collects/meta/build/nsis/plt-installer.ico create mode 100644 collects/meta/build/nsis/plt-installer.nsi create mode 100644 collects/meta/build/nsis/plt-uninstaller.ico create mode 100644 collects/meta/build/nsis/plt-welcome.bmp create mode 100755 collects/meta/build/patch-html create mode 100644 collects/meta/build/readme-specs create mode 100644 collects/meta/build/sitemap/AUTHORS create mode 100644 collects/meta/build/sitemap/COPYING create mode 100644 collects/meta/build/sitemap/ChangeLog create mode 100644 collects/meta/build/sitemap/PKG-INFO create mode 100644 collects/meta/build/sitemap/README create mode 100644 collects/meta/build/sitemap/example_config.xml create mode 100644 collects/meta/build/sitemap/example_urllist.txt create mode 100644 collects/meta/build/sitemap/plt-pre.xml create mode 100755 collects/meta/build/sitemap/setup.py create mode 100755 collects/meta/build/sitemap/sitemap_gen.py create mode 100755 collects/meta/build/sitemap/test_sitemap_gen.py create mode 100755 collects/meta/build/test-drscheme.ss create mode 100755 collects/meta/build/unix-installer/check-install-paths create mode 100644 collects/meta/build/unix-installer/paths-configure-snapshot create mode 100644 collects/meta/build/unix-installer/plt-installer-header create mode 100755 collects/meta/build/versionpatch diff --git a/collects/meta/build/build b/collects/meta/build/build new file mode 100755 index 0000000000..447de3e589 --- /dev/null +++ b/collects/meta/build/build @@ -0,0 +1,2094 @@ +#!/bin/sh + +## This is the build script which creates the pre-compiled directory. It is +## currently running from Eli's account on winooski, but it should be easy to +## configure to run anywhere. It updates the svn trees, so instead of running +## it straight from there it is better to copy it someplace else before running +## so we get clean copies. + +# if we're not using it already, switch to bash +if [ "${BASH_VERSION:-not_bash}" = "not_bash" ]; then exec bash "$0" "$@"; fi + +############################################################################### +### Configuration + +# verbose output? +verbose="yes" +# should we record an external log at $scriptlogfile? ("only" means only there) +scriptlog="yes" +# should we make binaries? +make_bins="ask_or_yes" +# should we do a repository update (and start with an empty iplt dir)? +make_repos="ask_or_yes" +# should we make the pdf docs directory? +make_pdf_docs="ask_or_yes" +# should we re-make the build directories? +make_builddirs="ask_or_yes" +# should we make the pre-install bundles? +make_bundles="ask_or_yes" +# should we make platform-specific installers? +make_installers="ask_or_yes" +# should we make stuff available on the web page? +# (for major distributions, it will be in html/NNN instead of html/) +make_web="ask_or_yes" +# should we run all test options? (multiple configurations) +run_all_tests="no" + +# people to notify when a build starts +buildnotifyemail="" + +# repository paths to use -- trunk, tags/..., or branches/... +init_svnpath_vars() { + # use this function to initialize these on remote builds too + svnpath="${PLTSVNPATH:-trunk}" + svnipath="${PLTSVNIPATH:-trunk}" +} +init_svnpath_vars + +# main machine that runs the whole build (the expected `$hostname' value) +workmachine="winooski" +# main directory on $workmachine (should be absolute) +maindir="/home/scheme" + +# machines for specific installer creations +dmgmachine="kauai" +nsismachine="pitcairn" + +# list of environment variables that should be carried over to ssh jobs +ssh_vars=(PLTSVNPATH PLTSVNIPATH) + +# Add stuff to be msetted later (when we have the `mset' function) +declare -a initial_msets machines +msets() { + local m; for m; do initial_msets[${#initial_msets[*]}]="$m"; done +} +# shorthand for mset to define a build target +defbuild() { + machines[${#machines[*]}]="$1" + msets "/machines/$1" "platform=$2"; shift 2; msets "$@" +} + +# Remote builds configuration, a table of /machines// +# entries, with misc fields set. Machines and platforms must be unique. The +# "D" first entry is for default field values (missing default makes the field +# required). Warning: an `eval "foo=\"bar\""' is used to assign values. +msets "/machines/D" "workdir=/var/tmp" "moveto=" "copytobak=" \ + "configure_args=" "LDFLAGS=" "ext_lib_paths=" "renice=" +# defbuild "ccs-solaris" "sparc-solaris" "moveto=/proj/scheme" \ +# "ext_lib_paths=/arch/unix/packages/openssl-0.9.7e" +defbuild "pitcairn" "i386-win32" \ + "workdir=f:" # no "/..." path (that can get interpreted as a flag) +# The LDFLAGS is a workaround for a bug in Fink, see +# http://wiki.finkproject.org/index.php/Fink:Packaging:Preparing_for_10.5#OpenGL_Bug +defbuild "kauai" "ppc-darwin" "configure_args=--enable-xonx" \ + "LDFLAGS=-dylib_file /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib:/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib" +defbuild "weatherwax" "ppc-osx-mac" \ + "configure_args=--enable-sdk=/Developer/SDKs/MacOSX10.4u.sdk" +defbuild "macintel" "i386-osx-mac" \ + "configure_args=--enable-sdk=/Developer/SDKs/MacOSX10.4u.sdk" +# defbuild "galaga" "i386-linux-ubuntu-hardy" +defbuild "champlain" "i386-linux-f12" +defbuild "ccs-linux" "i386-linux-ubuntu-jaunty" "moveto=/proj/scheme" +# defbuild "punge" "i386-linux-ubuntu-jaunty" "renice=20" +# defbuild "bjorn" "i386-linux-gcc2" +# defbuild "chicago" "i386-linux-debian" +defbuild "brownbuild" "i386-linux-debian" # really an AMD64 machine +# defbuild "inga" "i386-freebsd" +# defbuild "chicago-unstable" "i386-linux-debian-unstable" +# Start the main build last +defbuild "$workmachine" "x86_64-linux-f7" "copytobak=$maindir" +msets "/" + +############################################################################### +### Initialize & Setup environment + +ulimit -c 100000000 +umask 002 # stuff that is created should be r/w by the group + +# get this script's name and path +cd "`dirname \"$0\"`" +buildscript="`pwd`/`basename \"$0\"`" +# get the current hostname (short version) +hostname="`hostname`" +hostname="${hostname%%.*}" + +# svn repository url +svnroot="http://svn.plt-scheme.org" + +# web directory for pre-prelease stuff on $workmachine (relative to $maindir) +prewebdir="html" +# directory for installation (relative to $maindir) +installdir="plt" +# directory for internal stuff (relative to $maindir) +scriptdir="iplt" +# directories for clean repository checkouts (relative to $maindir) +cleandir="checkout" +cleanscriptdir="icheckout" +# directory for binaries (relative to $maindir) +bindir="binaries" +# directory for pre-installers (relative to $maindir) +preinstdir="pre-installers" +# directory for installers (relative to $maindir) +installersdir="installers" +# directory for pre-installers (relative to $maindir) +instdir="installers" +# directory for docs (relative to $maindir) +docdir="docs" +# directory for web content (relative to $maindir) +webdir="web" +# script for patching files with current version info +versionpatcher="$maindir/$scriptdir/build/versionpatch" +# DrScheme test script +drtestscript="$maindir/$scriptdir/build/test-drscheme.ss" +# bundling script +bundlescript="$maindir/$scriptdir/build/bundle" +# web build script +webscript="$maindir/$scriptdir/web/build.ss" +# html patching script +htmlpatchscript="$maindir/$scriptdir/build/patch-html" +# sitemap materials +sitemapdir="$maindir/$scriptdir/build/sitemap" + +# platform-installer stuff, directories and files are all absolute +nsisdir="$maindir/$scriptdir/build/nsis" +unixinstallerdir="$maindir/$scriptdir/build/unix-installer" +unixpathcheckscript="$unixinstallerdir/check-install-paths" +unixinstallerscript="$unixinstallerdir/plt-installer-header" + +# full clean tgz before building anything (relative to $maindir) +cleantgz="${installdir}-clean-tree.tgz" +# full plt/src tgz (relative to $maindir) +srctgz="$installdir-src.tgz" +# log file for this script (relative to $maindir) +scriptlogfile="build-log.txt" +# name of html files to generate for web directories +index="index.html" +# timestamp and version file for automatic scripts (relative to $maindir) +stampfile="stamp" + +# directory for temporary stuff (absolute path) -- on all machines +tmpdir="/tmp" +# lockfile for this script +lockfile="/tmp/plt-build-lock" +# name for running this script remotely +remotebuildscript="$tmpdir/build-plt" +# full name for clean repository tgz file to transfer for distributed builds +repostgz="$tmpdir/$cleantgz" +# full name for full tgz file (with binaries etc) +fulltgz="$tmpdir/$installdir-full.tgz" +# log file name prefix for background jobs +bglogfile="$tmpdir/plt-bg-log" + +last_part() { + echo "$*" | sed 's/.*[ -]//' +} +last_part_capital() { + local word="`last_part \"$@\"`" + echo "`echo \"${word:0:1}\" | tr \"[:lower:]\" \"[:upper:]\"`${word:1}" +} + +# simple name associations +name_of_platform() { + case "$1" in + ( "i386-linux" ) echo "Linux/GCC3" ;; + ( "i386-linux-gcc2" ) echo "Linux/GCC2" ;; + ( "i386-linux-fc2" ) echo "Linux/Fedora Core 2" ;; + ( "i386-linux-fc5" ) echo "Linux/Fedora Core 5" ;; + ( "i386-linux-fc6" ) echo "Linux/Fedora Core 6" ;; + ( "i386-linux-f7" ) echo "Linux/Fedora 7/i386" ;; + ( "x86_64-linux-f7" ) echo "Linux/Fedora 7/x86_64" ;; + ( "i386-linux-f9" ) echo "Linux/Fedora 9/i386" ;; + ( "i386-linux-f12" ) echo "Linux/Fedora 12/i386" ;; + ( "i386-linux-debian" ) echo "Linux/Debian-stable" ;; + ( "i386-linux-debian-testing" ) echo "Linux/Debian-testing" ;; + ( "i386-linux-debian-unstable" ) echo "Linux/Debian-unstable" ;; + ( "i386-linux-ubuntu" ) echo "Linux/Ubuntu" ;; + ( "i386-linux-ubuntu-"* ) echo "Linux/Ubuntu `last_part_capital \"$1\"`" ;; + ( "i386-freebsd" ) echo "FreeBSD" ;; + ( "sparc-solaris" ) echo "Solaris" ;; + ( "i386-osx-mac" ) echo "Mac OS X (Intel)" ;; + ( "ppc-osx-mac" ) echo "Mac OS X (PPC)" ;; + ( "ppc-darwin" ) echo "Mac X11 on Darwin (PPC)" ;; + ( "i386-darwin" ) echo "Mac X11 on Darwin (Intel)" ;; + ( "i386-win32" ) echo "Windows" ;; + # These are source distribution platforms + ( "unix" ) echo "Unix" ;; + ( "mac" ) echo "Macintosh" ;; + ( "win" ) echo "Windows" ;; + ( * ) exit_error "Unknown platform name for name_of_platform \"$1\"" ;; + esac +} +extra_description_of_platform() { + local e="" + case "$1" in + ( "i386-linux" ) e="Binaries for GCC3 (eg, RedHat 9 and Fedora Core)." ;; + ( "i386-linux-gcc2" ) e="Binaries for old GCC2 setups (eg, RedHat 7.x)." ;; + ( "i386-linux-fc2" ) e="A Linux build on Fedora Core 2." ;; + ( "i386-linux-fc5" ) e="A Linux build on Fedora Core 5." ;; + ( "i386-linux-fc6" ) e="A Linux build on Fedora Core 6." ;; + ( "i386-linux-f7" ) e="A Linux build on Fedora 7 (i386)." ;; + ( "x86_64-linux-f7" ) e="A Linux build on Fedora 7 (x86_64)." ;; + ( "i386-linux-f9" ) e="A Linux build on Fedora 9 (i386)." ;; + ( "i386-linux-f12" ) e="A Linux build on Fedora 12 (i386)." ;; + ( "i386-linux-debian" ) e="A Linux build on Debian Stable." ;; + ( "i386-linux-debian-testing" ) e="A Linux build on Debian Testing." ;; + ( "i386-linux-debian-unstable" ) e="A Linux build on Debian Unstable." ;; + ( "i386-linux-ubuntu" ) e="A Linux build on Ubuntu." ;; + ( "i386-linux-ubuntu-"* ) + e="A Linux build on Ubuntu (`last_part_capital \"$1\"`)." ;; + ( *"-osx-mac" ) e="An OS X Build." ;; + ( *"-darwin" ) e="This is an X11 on Darwin build using"; + e="$e--enable-xonx, not a standard OS X build." ;; + esac + if [[ "$e" != "" ]]; then echo "
${e}"; fi +} +name_of_dist_package() { + case "$1" in + ( "mz" ) echo "MzScheme" ;; + ( "plt" ) echo "PLT Scheme" ;; + ( "full" ) echo "PLT Scheme Full" ;; + ( * ) exit_error "Unknown package name for name_of_dist_package: \"$1\"" ;; + esac +} +name_of_dist_type() { + case "$1" in + ( "bin" ) echo "Binary" ;; + ( "src" ) echo "Source" ;; + ( * ) exit_error "Unknown type name for name_of_dist_type: \"$1\"" ;; + esac +} +platforms_of_dist_type() { + case "$1" in + ( "bin" ) echo "i386-win32" \ + "i386-osx-mac" \ + "ppc-osx-mac" \ + "ppc-darwin" \ + "i386-darwin" \ + "i386-linux" \ + "i386-linux-gcc2" \ + "i386-linux-fc2" \ + "i386-linux-fc5" \ + "i386-linux-fc6" \ + "i386-linux-f7" \ + "x86_64-linux-f7" \ + "i386-linux-f9" \ + "i386-linux-f12" \ + "i386-linux-debian" \ + "i386-linux-debian-testing" \ + "i386-linux-debian-unstable" \ + "i386-linux-ubuntu" \ + "i386-linux-ubuntu-dapper" \ + "i386-linux-ubuntu-edgy" \ + "i386-linux-ubuntu-feisty" \ + "i386-linux-ubuntu-hardy" \ + "i386-linux-ubuntu-intrepid" \ + "i386-linux-ubuntu-jaunty" \ + "i386-freebsd" \ + "sparc-solaris" ;; + ( "src" ) echo "win mac unix" ;; + ( * ) exit_error "Unknown type name for platforms_of_dist_type: \"$1\"" ;; + esac +} +installer_of_dist_type_platform() { # input: dtype-dplatform + case "$1" in + ( "src-unix" ) echo "tgz" ;; + ( "src-mac" ) echo "dmg" ;; + ( "src-win" ) echo "zip" ;; + ( "bin-"*"-linux"* ) echo "sh" ;; + ( "bin-"*"-freebsd" ) echo "sh" ;; + ( "bin-"*"-solaris" ) echo "sh" ;; + ( "bin-"*"-darwin" ) echo "sh" ;; + ( "bin-"*"-osx-mac" ) echo "idmg" ;; + ( "bin-"*"-win32" ) echo "exe" ;; + ( * ) exit_error "Unknown dist type+platform for" \ + "installer_of_dist_type_platform: \"$1\"" ;; + esac +} +explanation_of_installer_type() { + case "$1" in + ( "tgz" ) echo "Unpack this file using" \ + "\"gunzip | tar xvf -\"." ;; + ( "dmg" ) echo "Mount this disk image and copy the PLT folder to your" \ + "disk." ;; + ( "idmg" ) echo "Some browsers will automatically mount & copy the" \ + "\"PLT Scheme\" folder to your desktop; if yours" \ + "does not, mount the disk and copy it yourself." ;; + ( "zip" ) echo "Use unzip to extract the PLT folder to your disk." ;; + ( "sh" ) echo "Execute this file with \"sh \"," \ + "and follow the instructions." ;; + ( "exe" ) echo "This is a standard Windows installer." ;; + ( * ) exit_error "Unknown installer type for" \ + "explanation_of_installer_type: \"$1\"." ;; + esac +} + +# This is for running mzscheme scripts, unrelated to the build itself +export PLTHOME="$maindir/$installdir" \ + PLT_EXTENSION_LIB_PATHS="" \ + PLTPLANETDIR="/tmp/plt-build-planet" +export PATH="$PLTHOME/bin:$PATH" +unset PLTCOLLECTS; export PLTCOLLECTS + +# useful for tests etc +export PLT_BUILD="yes" + +# setup for gui tests (and outside of them, there will not be a :65 +# display, so trying any gui will fail) +real_DISPLAY="$DISPLAY" +export DISPLAY=":65" +if [[ "$XAUTHORITY" = "" ]]; then export XAUTHORITY="$HOME/.Xauthority"; fi + +############################################################################### +### Utilities + +no_exit_on_error="no" +exit_error() { + echo "" + echo "<<>> (Working on ${machine}(${platform}))" 1>&2 + echo "$@" 1>&2 + if [[ "$no_exit_on_error" = "yes" ]]; then + echo "" + else + echo "Aborting" 1>&2 + exit 1 + fi +} +dont_exit() { + no_exit_on_error="yes" ; "$@" ; no_exit_on_error="no" +} + +cleanup_lockfile() { + rm -f "$lockfile" +} + +# Utilities for multi-level variables that can be used as sort of an +# associative arrays, with names that are treated similarly to paths and a +# default context similar to the current directory. (Implemented as plain +# variables, using "__" as the translation of "/" level separators.) +shopt -s extglob # needed for some hacks below +mcontext="/" # the current context for m-ops +mset() { + # mset goes over all args, which can have the following shapes: + # ...=... sets a variable in the current context + # /.../... sets the current absolute context + # .../... sets the current relative context + local m mvar val + for m; do + case "$m" in + ( *=* ) mvar="${m%%=*}" val="${m#*=}" + normalize_mvar; obfuscate_mvar + eval "${mvar}=\"${val}\"" + ;; + ( */* ) mvar="$m"; normalize_mvar; mcontext="$mvar" ;; + ( * ) exit_error "unknown name in mset: $m" ;; + esac + done +} +mget() { + # mget crawls over all args, and for each one retreives the mvar into a plain + # variable. The full form of an arg is "tgt=mvar?def" for a specified target + # var (default is the mvar's basename), and a default. The default can start + # with `@' to make it another mvar reference + local m mvar tgt def nodef=" <<>> " + for m; do + mvar=""; tgt=""; def="$nodef" + if [[ "$m" = *=* ]]; then tgt="${m%%=*}"; m="${m#*=}"; fi + if [[ "$m" = *"?"* ]]; then def="${m#*[?]}"; m="${m%%[?]*}"; fi + mvar="$m"; normalize_mvar + if [[ "$tgt" = "" ]]; then tgt="${mvar##*/}"; fi + obfuscate_mvar + if [[ "$def" = "$nodef" ]]; then + eval "${tgt}=\"\${${mvar}?${m} is not set}\"" + else + local R="$nodef" + eval "R=\"\${${mvar}:-\"$R\"}\"" + if [[ "$R" != "$nodef" ]]; then eval "${tgt}=\"${R}\"" + elif [[ "$def" = "@"* ]]; then mget "${tgt}=${def#@}" + else eval "${tgt}=\"${def}\"" + fi + fi + done +} +machineget() { + # an mget-like version for machines, using the default fields (and a global + # $machine value) + local m tgt + for m; do + if [[ "$m" = *=* ]]; then tgt="${m%%=*}="; m="${m#*=}"; else tgt=""; fi + mget "${tgt}/machines/${machine}/${m}?@/machines/D/${m}" + done +} +# Utility for the above: normalize `mvar' (mvar and mcontext are globals) +normalize_mvar() { + # absolute mvar => don't use the mcontext + if [[ ! "$mvar" = "/"* ]]; then mvar="/${mcontext}/${mvar}"; fi + mvar="${mvar}/" # add "/" suffix for the processing below + mvar="${mvar//\/+(\/)//}" # "//" -> "/" + mvar="${mvar//\/.\///}" # "/./" -> "/" + mvar="${mvar//\/+([^\/])\/..\///}" # eliminate ".." + mvar="${mvar/#\/+(..\/)//}" # eliminate prefix ".." + mvar="${mvar%/}" # remove "/" suffix +} +obfuscate_mvar() { + mvar="${mvar//\//__}" + mvar="${mvar//-/_}" +} +# now that we have these functions, do the initial_msets +mset "${initial_msets[@]}" +# global build-context variables, and set main-machine values +machine="$workmachine" +machineget platform workdir + +# portable `echo -n' +if [[ "`echo -n`" = "-n" ]]; then + echo_n() { echo ${1+"$@"}"\c"; } +else + echo_n() { echo -n ${1+"$@"}; } +fi + +show() { + if [[ "$verbose" = "yes" ]]; then + echo "" + case "$platform" in + ( *"-linux"* | "sparc-solaris" | "i386-win32" ) + echo ">>>" "$@" | fmt -t -w 79 + ;; + ( *"-freebsd" | *"-osx-mac" | *"-darwin" ) + echo ">>>" "$@" | fmt -w 79 + ;; + ( * ) + echo ">>>" "$@" | fmt + ;; + esac + fi +} + +# a yes/no question mode for some vars, possibly set a constant answer +ask_mode="no" +fixed_reply="" +is_yes() { + local var="$1"; shift + local val; eval val="\$$var" + local reply + if [[ "$val" = "yes" ]]; then return 0 + elif [[ "$val" = "no" ]]; then return 1 + elif [[ "$val" = "ask_or_yes" ]]; then + if [[ "$ask_mode" = "yes" ]]; then + echo "" 1>&2 + echo "" 1>&2 + while true; do + echo_n ">>> QUESTION >>> $var [y/n/Y/N] ? " 1>&2 + if [[ "$fixed_reply" != "" ]]; then reply="$fixed_reply" + else read -sn 1 reply; fi + echo "$reply" 1>&2 + case "$reply" in + ( Y ) fixed_reply="y"; reply="y" ;; + ( N ) fixed_reply="n"; reply="n" ;; + esac + case "$reply" in + ( y ) eval $var="yes"; return 0 ;; + ( n ) eval $var="no"; return 1 ;; + ( * ) reply="" ;; + esac + done + else + eval $var="yes"; return 0 + fi + else + exit_error "bad value for flag '$var': '$val'" + fi +} + +lookfor() { + save_IFS="${IFS}" + IFS="${IFS}:" + for dir in $PATH; do + if test -x "$dir/$1"; then + IFS="$save_IFS" + echo_n "$dir/$1" + return + fi + done + IFS="$save_IFS" +} + +_run() { + show "Running \"$*\"" + "$@" \ + || exit_error "Errors when running \"$*\"" +} + +# there is a common sh hack for getting the Nth word from a command: +# "... `set \`blah\`; echo $1` ..." +# the problem with this is if blah produces no output -- which will end up +# dumping out the complete environment -- so use this instead +__get_first_arg() { printf '%s' "$1"; } +__get_first_output() { __get_first_arg `cat`; } +# inputs: command to run +get_first() { "$@" | __get_first_output; } + +_cd() { + local OLDWD="`pwd`" + cd "$1" || exit_error "Could not cd into \"$1\"" + local NEWWD="`pwd`" + if [[ "$NEWWD" != "$OLDWD" ]]; then + show "Now in \"`pwd`\"" + fi +} + +_md() { + for x; do + if [[ ! -d "$x" ]]; then + show "Creating directory \"$x\"" + mkdir -p "$1" || exit_error "Could create directory \"$x\"" + fi + done +} + +_mcd() { + _md "$1"; _cd "$1" +} + +_rm() { + for x; do + if [[ -h "$x" ]]; then + show "Deleting link \"$x\"" + rm -f "$x" || exit_error "The \"$x\" link cannot be deleted" + elif [[ -d "$x" ]]; then + show "Deleting directory \"$x\"" + rm -rf "$x" || exit_error "The \"$x\" directory cannot be deleted" + elif [[ -e "$x" ]]; then + show "Deleting \"$x\"" + rm -rf "$x" || exit_error "\"$x\" cannot be deleted" + fi + done +} + +_rmd() { + _rm "$1"; _md "$1" +} + +_rmcd() { + _rm "$1"; _mcd "$1" +} + +_mv() { + show "Moving \"$*\"" + mv "$@" || exit_error "Could not move \"$*\"" +} + +_cat() { + show "Showing \"$@\"" + cat "$@" || exit_error "Could not show \"$@\"" +} + +_cp() { + show "Copying: \"$*\"" + cp -p "$@" || exit_error "Could not copy \"$*\"" +} + +_scp() { + show "Copying: \"$*\"" + scp -p "$@" || exit_error "Could not copy \"$*\"" +} + +_ln() { + show "SymLinking \"$2\" -> \"$1\"" + ln -s "$1" "$2" || exit_error "Could not symlink \"$2\"->\"$1\"" +} + +_zip() { + local zip_file="$1"; shift + show "Zipping \"$*\" to \"$zip_file\" in \"`pwd`\"" + zip -qr9 "$zip_file" "$@" \ + || exit_error "Could not zip \"$*\" to \"$zip_file\" in \"`pwd`\"" +} + +# try to use gtar if we can find it +TAR="`lookfor gtar`" +if [[ "$TAR" = "" ]]; then TAR="`lookfor tar`"; fi + +_tar() { + local tar_file="$1"; shift + show "Tarring \"$*\" to \"$tar_file\" in \"`pwd`\"" + "$TAR" cf "$tar_file" "$@" \ + || exit_error "Could not tar \"$*\" to \"$tar_file\" in \"`pwd`\"" +} + +_tgzip() { + local tgz_file="$1"; shift + show "Packing \"$*\" to \"$tgz_file\" in \"`pwd`\"" + "$TAR" czf "$tgz_file" "$@" \ + || exit_error "Could not pack \"$*\" to \"$tgz_file\" in \"`pwd`\"" +} + +_tar_add() { + local tar_file="$1"; shift + show "Adding \"$*\" to \"$tar_file\" in \"`pwd`\"" + "$TAR" uf "$tar_file" "$@" \ + || exit_error "Could not add \"$*\" to \"$tar_file\" in \"`pwd`\"" +} + +_tgunzip() { + show "Unpacking \"$1\" in \"`pwd`\"" + "$TAR" xzf "$1" || exit_error "Could not unpack \"$1\" in \"`pwd`\"" +} + +_tgunzipm() { + show "Unpacking \"$1\" in \"`pwd`\"" + "$TAR" xzmf "$1" || exit_error "Could not unpack \"$1\" in \"`pwd`\"" +} + +_strip() { + local f + for f; do + if [[ -e "$f" ]]; then + show "Stripping \"$f\"" + strip -S "$f" || exit_error "Could not strip \"$f\"" + fi + done +} + +svn_get() { # inputs: svn repository, svn path, path in $maindir + local repos="$1" path="$2" dir="$3"; shift 3 + show "Getting $repos/$path to $maindir/$dir" + _cd "$maindir" + if [[ ! -d "$dir" ]]; then + _run svn checkout --depth immediates "$svnroot/$repos" "$dir" + fi + _cd "$dir" + _run svn update --set-depth infinity "$path" + svn status "$path" > "$tmpdir/svn-st" \ + || exit_error "problems running svn status" + if [[ -s "$tmpdir/svn-st" ]]; then + cat "$tmpdir/svn-st" 1>&2 + rm -f "$tmpdir/svn-st" + exit_error "The working directory is not clean (see above)" + fi + rm -f "$tmpdir/svn-st" + _cd "$maindir" +} + +append_dots() { # inputs: width, string + local line="............................................................" + echo "${2}${line:0:$(( ${1} - ${#2} ))}" +} + +separator() { + local line="============================================================" + local sep="$*" + local sep_len=${#sep} + local idx1=$(( ( 77 - $sep_len ) / 2 )) + local idx2=$(( ( 78 - $sep_len ) / 2 )) + local line1=${line:0:$(( ( $idx1 < 3 ) ? 3 : $idx1 ))} + local line2=${line:0:$(( ( $idx2 < 3 ) ? 3 : $idx2 ))} + local dashes="`echo \"$line1 $sep $line2\" | sed 's/./-/g'`" + echo "" + echo "" + echo "$dashes" + echo "$line1 $sep $line2" + echo "$dashes" + echo "" +} + +build_step() { # inputs: name, command + local jobname="$1"; shift + separator "Building: $jobname [${machine}(${platform})]" + show "Running \"$*\"" + start_timer + "$@" || exit_error "\"$jobname\" part of build process failed" + show_time "--==> $jobname on ${machine}(${platform}) done," +} + +cur_secs() { + date '+%s' +} +start_timer() { + timer_start=`cur_secs` +} +show_time() { + local time=$(( `cur_secs` - $timer_start )) + local secs=$(( $time % 60 )) + local mins=$(( $time / 60 )) + show "$1 time: `printf '%d:%02d' $mins $secs`" +} + +choose_for_testing() { # input: test_mode, options ... + # choose items from the given inputs, either all, the first, or a random one + local mode="$1"; shift + case "$mode" in + ( all ) echo "$*" ;; + ( def ) echo "$1" ;; + ( rnd ) mode=$(( $RANDOM % $# + 1 )); echo "${!mode}" ;; + ( * ) exit_error "bad value in choose_for_testing: $mode" + esac +} + +# Utilities for GUI tests (and process management) + +_kill() { # args: pid [process name] + local pid="$1"; shift + local desc="$pid" + if [[ "$1" != "" ]]; then desc="$1 ($pid)"; shift; fi + if [[ ! -d "/proc/$pid" ]]; then return; fi + show "Killing $desc" + kill -15 "$pid" > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi + usleep 500000 ; if [[ ! -d "/proc/$pid" ]]; then return; fi + usleep 500000 ; if [[ ! -d "/proc/$pid" ]]; then return; fi + sleep 1 ; if [[ ! -d "/proc/$pid" ]]; then return; fi + echo "re-killing $desc" + kill -15 "$pid" > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi + sleep 2 ; if [[ ! -d "/proc/$pid" ]]; then return; fi + echo "re-re-killing $desc" + kill -15 "$pid" > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi + sleep 2 ; if [[ ! -d "/proc/$pid" ]]; then return; fi + echo "murdering $desc" + kill -9 "$pid" > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi + sleep 2 ; if [[ ! -d "/proc/$pid" ]]; then return; fi + echo "re-murdering $desc" + kill -9 "$pid" > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi + sleep 2 ; if [[ ! -d "/proc/$pid" ]]; then return; fi + echo "re-re-murdering $desc" + kill -9 "$pid" > /dev/null 2>& 1; if [[ ! -d "/proc/$pid" ]]; then return; fi + sleep 2 ; if [[ ! -d "/proc/$pid" ]]; then return; fi + echo "BOOM Zombie alert: $desc did not die" +} + +_timeout_run() { # first input is the timeout + local timeout="$1"; shift + local exe="$1" + show "Running \"$*\" with a timeout of $timeout" + "$@" & + local pid="$!" + local result="99" + ( # sleep in background so we're still interruptible + local sleeper="$$" + alldone() { kill -15 "$sleeper"; exit; } + trap alldone 0 3 9 15 + sleep "$timeout" & + sleeper="$!" + wait "$sleeper" + _kill "$pid" "$exe [timeout]" + ) & + local killerpid="$!" + wait "$pid"; result="$?" + _kill "$killerpid" + if [[ "$result" != "0" ]]; then + exit_error "Errors when running \"$*\" (with a timeout)" + fi + return "$result" +} + +Xvncpid="" +Xwmpid="" +_start_xvnc() { + local xvnclog="$tmpdir/plt-xvnc-log" + show "Starting Xvnc (logfile at \"$xvnclog\")" + # Create Xauth cookie + cookie="`mcookie`" + xauth -f "$XAUTHORITY" add "`uname -n`$DISPLAY" . "$cookie" + xauth -f "$XAUTHORITY" add "`uname -n`/unix$DISPLAY" . "$cookie" + # Create Xvnc session, with a WM + Xvnc "$DISPLAY" \ + -rfbport 6565 \ + -localhost \ + -desktop "PLT-Session" \ + -geometry 1024x768 \ + -depth 16 \ + -httpPort=0 \ + -auth "$XAUTHORITY" \ + -rfbauth "$HOME/.vnc/passwd" \ + -br \ + > "$xvnclog" 2>&1 & + Xvncpid="$!"; usleep 500000 + echo "Xvnc running ($Xvncpid)" + metacity --sm-disable & + Xwmpid="$!"; usleep 500000 + echo "window manager running ($Xwmpid)" + # to see the window, uncomment this + # DISPLAY="$real_DISPLAY" vncviewer ::6565 -PasswordFile "$HOME/.vnc/passwd" & +} +_end_xvnc() { + show "Killing Xvnc session" + if [[ "$Xvncpid" = "" ]]; then show "Xvnc was not started"; return 1; fi + _kill "$Xwmpid" "window manager" + _kill "$Xvncpid" "Xvnc" + Xvncpid=""; Xwmpid="" +} + +parse_c_define() { # input: filename, varname + local file="$1" varname="$2"; shift 2 + grep "^ *# *define * $varname * " "$file" \ + | sed -e 's/^ *# *define * [^ ]* * //' -e 's/ * $//' +} + +version_init() { # input: plthome + local vfile="$1/src/mzscheme/src/schvers.h" + [[ -e "$vfile" ]] \ + || exit_error "Could not find version file at \"$vfile\"" + # parse version info + version="`parse_c_define \"$vfile\" MZSCHEME_VERSION | sed -e 's/\"//g'`" + version1="`parse_c_define \"$vfile\" MZSCHEME_VERSION_X`" + version2="`parse_c_define \"$vfile\" MZSCHEME_VERSION_Y`" + version3="`parse_c_define \"$vfile\" MZSCHEME_VERSION_Z`" + version4="`parse_c_define \"$vfile\" MZSCHEME_VERSION_W`" + # consistency check + local VER="$version1.$version2" + if [[ "$version4" != "0" ]]; then VER="$VER.$version3.$version4" + elif [[ "$version3" != "0" ]]; then VER="$VER.$version3" + fi + [[ "$version" = "$VER" ]] \ + || exit_error "Mismatch in \"$vfile\": $version vs $VER" + # release is when the last one is zero + if [[ "$version4" = "0" ]]; then + separator "This is a release version ($version)" + releasing="yes" + reallyreleasing="yes" + elif [[ "$svnpath" = "release" ]]; then + separator "This is a pre-release version ($version)" + releasing="yes" + reallyreleasing="no" + else + separator "This is a non-release version ($version)" + releasing="no" + reallyreleasing="no" + fi +} + +# html functions -- all write to $htmloutput +# ($htmloutput is usually $index; also, assume that $htmloutput is in the +# current directory -- be careful when cd-ing!) +# stuff before html_content_begin and after html_content_end is temporary, +# later on, patch-html will combine the contents with the skeleton files. +html_begin() { # inputs: title [output-name] + local htmltitle="$1"; shift + htmloutput="$index" + if [[ "$1" != "" ]]; then htmloutput="$1"; shift; fi + show "Creating \"`pwd`/$htmloutput\" for \"$htmltitle\"" + _rm "$htmloutput" + { echo "" + echo "$htmltitle" + echo "" + echo "" + echo " " + echo " " + echo "
" + echo " " + echo " " + echo "
" + echo " $htmltitle

" + while [[ "$#" -gt "0" ]]; do + if [[ "$1" = "-f" ]]; then shift; cat "$1"; else echo "$1"; fi + shift + done + } > "$htmloutput" +} +html_content_begin() { + echo '' >> "$htmloutput" +} +html_table_begin() { # inputs: [rules-attr] + local rules="rows" + if [[ "$1" != "" ]]; then rows="$1"; fi + { echo "
" + echo "" + } >> "$htmloutput" +} +html_show() { # inputs: or <-f file> ... + { while [[ "$#" -gt "0" ]]; do + if [[ "$1" = "-f" ]]; then shift; cat "$1"; else echo "$1"; fi + shift + done + } >> "$htmloutput" +} +html_file_row() { # inputs: filename, explanation ... + local fname="$1"; shift + { echo_n "" + echo "" + } >> "$htmloutput" +} +html_table_end() { + echo "
• " + echo_n "$fname" + if [[ -f "$fname" ]]; then + echo_n " (`get_first du -h \"$fname\"`)" + fi + echo " $*
" >> "$htmloutput" +} +html_content_end() { + echo '' >> "$htmloutput" +} +html_end() { + { echo "
" + echo '' + echo "(version $version, $htmltimestamp)" + echo '' + echo "
" + echo "" + } >> "$htmloutput" + show "Finished \"`pwd`/$htmloutput\"" +} + +run_part() { + local exec=no + local bg=no + while true; do + case "$1" in + ( -exec ) exec="yes"; shift; continue ;; + ( -bg ) bg="yes"; shift; continue ;; + ( * ) break ;; + esac + done + local runhost="$1" runpart="$2"; shift 2 + # echo "runhost=$runhost, exec=$exec, bg=$bg, $*" + if [[ "$runhost" = "$hostname" ]]; then + if [[ "$bg" = "yes" ]]; then "$runpart" "$@" & + # must check $exec before running -- since if this is done in bg, then + # other calls to this function will overwrite it! + elif [[ "$exec" = "yes" ]]; then "$runpart" "$@"; exit $? + else "$runpart" "$@" + fi + else + # ssh does not preserve proper arguments, so this does not work with + # arguments that contain spaces. + local rbuild="$remotebuildscript" + _scp "$buildscript" "${runhost}:$remotebuildscript" + local ssh_vars_vals i var val + i=0 + while [[ "$i" -lt ${#ssh_vars[*]} ]]; do + var="${ssh_vars[i]}" + eval "val=\"\${$var}\"" + ssh_vars_vals[$i]="${var}=${val}" + i=$((i+1)) + done + local cmd + cmd="--dispatch ${ssh_vars_vals[@]} $runhost $runpart" + if [[ "$exec" = "yes" ]]; then + exec ssh "$runhost" "$rbuild" $cmd "$@" \ + || exit_error "Errors running \"$rbuild\" on \"$runhost\"" + exit_error "Something is wrong with \"exec\"" + elif [[ "$bg" = "yes" ]]; then + ssh "$runhost" "$rbuild" $cmd "$@" & + else + ssh "$runhost" "$rbuild" $cmd "$@" \ + || exit_error "Errors running \"$rbuild\" on \"$runhost\"" + fi + fi +} + + +############################################################################### +### Build Parts + +MAIN() { + # switch to build machine, if invoked remotely + run_part -exec "$workmachine" MAIN_BUILD "$@" +} + +## ============================================================================ + +MAIN_BUILD() { + + ## -------------------------------------------------------------------------- + # use a lock file, no retries, and recreate it if it's over 3 hours old + _run lockfile -r 0 -l 10800 "$lockfile" + trap cleanup_lockfile 0 3 9 15 + + ## -------------------------------------------------------------------------- + separator "Begin (`date`)" + + timestamp="`date '+%Y%m%d%H%M'`" + htmltimestamp="`date '+updated at %A, %B %d %Y, %H:%M %Z'`" + if [[ "$1" = "ask" ]]; then ask_mode="yes"; shift; fi + + ## -------------------------------------------------------------------------- + if is_yes make_repos; then + separator "Repository updates" + svn_get "plt" "$svnpath" "$cleandir" + svn_get "iplt" "$svnipath" "$cleanscriptdir" + else + show "Skipping repository updates" + fi + + version_init "$maindir/$cleandir/$svnpath" + if is_yes make_repos; then + DO_AUTO_UPDATES + fi + + if is_yes make_repos; then + _cd "$maindir" + _rm "$scriptdir" + _cp -r "$cleanscriptdir/$svnipath" "$scriptdir" + fi + + if is_yes make_bins; then + _rm "$repostgz" + _cd "$maindir/$cleandir/$svnpath" + _tgzip "$repostgz" --exclude=".svn" * + _cd "$maindir" + fi + + # send build notification message + if [[ "$buildnotifyemail" != "" && "$CRON" != "yes" ]]; then + show "Sending notifications" + echo "Build starting at `date`" \ + | mail -s "A build is starting..." "$buildnotifyemail" + fi + + ## -------------------------------------------------------------------------- + separator "Dispatching build jobs" + + local m + if is_yes make_bins; then + for m in "${machines[@]}"; do DO_COPY_BUILD "$m"; done + else + show "Skipping binaries" + fi + + # build pdfs while other machines continue doing their builds + BUILD_DOCS_AND_PDFS + + # and now wait for all builds + if is_yes make_bins; then + show "Waiting for remote jobs to finish" + wait + for m in "${machines[@]}"; do + machine="$m" + machineget mplatform=platform + if [[ "$machine" != "$workmachine" ]]; then + separator "{{{ Doing ${machine}(${mplatform}) remotely }}}" + _cat "$bglogfile-$machine" + _rm "$bglogfile-$machine" + fi + done + fi + + if is_yes make_builddirs; then COPY_AND_BUILD_BINARY_DIRS + else show "Skipping copying and dirs"; fi + + if is_yes make_bundles; then BUILD_BUNDLES + else show "Skipping bundles"; fi + + if is_yes make_installers; then BUILD_INSTALLERS + else show "Skipping installers"; fi + + if is_yes make_web; then BUILD_WEB; fi + + _rm "$lockfile" + + separator "Done (`date`)" + +} + +## ============================================================================ + +DO_AUTO_UPDATES() { + + ## -------------------------------------------------------------------------- + separator "Updating repository files" + + show "Updating stamp file" + _cd "$maindir/$cleandir/$svnpath" + local stamp="collects/repos-time-stamp/stamp.ss" + _rm "$stamp" + show "Creating $stamp" + { echo_n '#lang scheme/base (provide stamp) (define stamp "' + echo_n "`date +'%e%b%Y' | tr -d ' ' | tr 'A-Z' 'a-z'`" + echo '")' + } > "$stamp" + + show "Updating version numbers" + # if the racket executable is not there, we'll fail, but that + # shouldn't be a problem since it will run again next time + if [[ -x "$PLTHOME/bin/racket" ]]; then + dont_exit _run "$versionpatcher" "$version" + else + show "Skipping version update (no racket executable)" + fi + + show "Committing changes (if any)" + _run svn commit -m "Welcome to a new PLT day." . + +} + +## ============================================================================ + +DO_COPY_BUILD() { # inputs -- machine-name (for ssh) + + ## -------------------------------------------------------------------------- + machine="$1"; shift + + if [[ "$machine" != "$workmachine" ]]; then + show "Running DO_BUILD on $machine in the background" + _scp "$repostgz" "${machine}:$repostgz" + _rm "$bglogfile-$machine" + run_part -bg "$machine" "DO_BUILD" "$releasing" "$@" \ + &> "$bglogfile-$machine" + else + separator "{{{ Doing ${machine}(${platform}) locally }}}" + run_part "$machine" "DO_BUILD" "$releasing" "$@" + fi + +} + +## ============================================================================ + +DO_BUILD() { # inputs -- releasing + + ## -------------------------------------------------------------------------- + releasing="$1"; shift + machineget platform workdir moveto copytobak \ + configure_args ext_lib_paths renice + + if [[ "$renice" != "" ]]; then dont_exit _run renice "$renice" "$$"; fi + + export PLTHOME="$workdir/$installdir" PATH="$PLTHOME/bin:$PATH" + export SETUP_ARGS="-l- setup -U" + + # make sure we don't use any planet caches (PLTPLANETDIR is set globally) + _rm "$PLTPLANETDIR" + + if [[ "$releasing" = "yes" ]]; then + # don't do this for the nightly builds -- if they fail and a previous tgz + # is there, we'll end up using it + _rm "$fulltgz" + fi + + if [[ ext_lib_paths != "" ]]; then + export PLT_EXTENSION_LIB_PATHS="${ext_lib_paths}:$PLT_EXTENSION_LIB_PATHS" + fi + + if [[ "$machine" != "$workmachine" ]]; then + _rmcd "$PLTHOME" + _tgunzipm "$repostgz" + export SETUP_ARGS="$SETUP_ARGS -D" + else + # on the main machine, copy the repository to keep meta .svn information + _cd "$workdir" + _rm "$PLTHOME" + _cp -r "$maindir/$cleandir/$svnpath" "$PLTHOME" + # and then create pre-build archives + show "Creating pre-build archives" + _tgzip "$maindir/$cleantgz" --exclude=".svn" "$installdir" + _tgzip "$maindir/$srctgz" --exclude=".svn" "$installdir/src" + fi + + ## -------------------------------------------------------------------------- + if [[ "$platform" = "i386-win32" ]]; then + export PLTPLANETDIR="`cygpath -w \"$PLTPLANETDIR\"`" + DO_WIN32_BUILD + else + _mcd "$PLTHOME/src/build" + machineget LDFLAGS; export LDFLAGS + build_step "configure" ../configure ${configure_args} + build_step "make both" make both + build_step "make install" make plain-install-both + build_step "setup-plt" "$PLTHOME/bin/racket" $SETUP_ARGS + fi + + ## -------------------------------------------------------------------------- + separator "${machine}(${platform}): Stripping binaries" + + # Strip binaries + _cd "$PLTHOME" + case "$platform" in + ( *"-linux"* | *"-freebsd" | "sparc-solaris" | *"-darwin" ) + _strip "bin/racket"{,3m,cgc} "bin/gracket"{,3m,cgc} + ;; + ( *"-osx-mac" ) + _strip "bin/racket"{,3m,cgc} "GRacket"*".app/Contents/MacOS/GRacket"* \ + "lib"/{,G}"Racket.framework"/"Versions"/*/{,G}"Racket" + ;; + ( *"-win32" ) + # (just don't include *.pdb and *ilk) + show "Nothing to strip for \"$platform\"" + ;; + ( * ) + exit_error "don't know if binaries for $platform should be stripped" + ;; + esac + + ## -------------------------------------------------------------------------- + separator "${machine}(${platform}): Creating \"$fulltgz\"" + + _rm "$fulltgz" + _cd "$workdir" + # excluding x/y does not work on solaris, so rename it instead + _mv "$PLTHOME/src" "$PLTHOME/___src___" + _tgzip "$fulltgz" --exclude="___src___" --exclude=".svn" \ + --exclude="*.[Pp][Dd][Bb]" --exclude="*.[Ii][Ll][Kk]" \ + "$installdir" + _mv "$PLTHOME/___src___" "$PLTHOME/src" + + ## -------------------------------------------------------------------------- + # choose a test mode (def/rnd/all) + local test_mode="def" + if [[ "$run_all_tests" = "yes" ]]; then test_mode="all"; + elif [[ "$releasing" = "yes" ]]; then test_mode="all"; + elif [[ "$(( $RANDOM % 2 ))" = "0" ]]; then test_mode="rnd"; + fi; + separator "${machine}(${platform}) testing Racket ($test_mode)" + local testdir="$tmpdir/mztests" + _rmcd "$testdir" + + local _exe _jit exe flags + for _exe in `choose_for_testing $test_mode 3m cgc`; do + for _jit in `choose_for_testing $test_mode yes no`; do + if [[ "${_exe}" = "cgc" ]]; then exe="cgc"; else exe=""; fi + if [[ "$platform" = "i386-win32" ]]; then + exe="$PLTHOME/Racket$exe.exe" + else + exe="$PLTHOME/bin/racket$exe" + fi + flags="" + if [[ "${_jit}" = "no" ]]; then flags="--no-jit $flags"; fi + dont_exit _run env HOME="$testdir" DISPLAY="" \ + "$exe" $flags "$PLTHOME/collects/tests/run-automated-tests.ss" + done + done + sleep 8 # time to flush stderr + + # MrEd-based tests on the main machine, in an Xvnc session + if [[ "$machine" = "$workmachine" ]]; then + separator "${machine}(${platform}) running Mred/DrScheme tests" + _start_xvnc + dont_exit _timeout_run 60 env HOME="$testdir" "$drtestscript" + dont_exit _timeout_run 300 env HOME="$testdir" \ + "$PLTHOME/collects/tests/framework/framework-test" + _end_xvnc + fi + + ## -------------------------------------------------------------------------- + # move to the target at the end of the build, only if building from trunk + local targetdir="" mode="" op="" + if [[ "$svnpath" != "trunk" ]]; then : + elif [[ "$copytobak" != "" ]]; then + targetdir="$copytobak"; mode="bk"; op="Backing up" + elif [[ "$moveto" != "" ]]; then + targetdir="$moveto"; mode="mv"; op="Moving" + fi + if [[ "$targetdir" != "" ]]; then + separator "${machine}(${platform}): $op installation to \"$targetdir\"" + _md "$targetdir/$installdir-new" + _cd "$workdir/$installdir" + show "Copying \"$PLTHOME\" to \"$targetdir/$installdir-new\"" + "$TAR" cf - . | ( cd "$targetdir/$installdir-new"; "$TAR" xf - ) \ + || exit_error \ + "Could not copy \"$PLTHOME\" to \"$targetdir/$installdir-new\"" + _cd "$targetdir" + if [[ "$mode" = "mv" ]]; then + # move the installation, trying to delete the previous one if possible + # do it this way in case there is already a leftover "$installdir-old" + _md "$installdir-old" + _mv "$installdir" "$installdir-old/old-`date '+%Y%m%d%H%M'`-$$" + _mv "$installdir-new" "$installdir" + _rm "$PLTHOME" + show "Removing \"$targetdir/$installdir-old\"" + # this is done this way in case there is an old process using a binary + # which will not allow removing the directory, but we don't care about + # that. + dont_exit _rm "$targetdir/$installdir-old" + else + # copy the installation to a backup directory, leaving one + # backup of the old backup tree if it was there (this is used on + # the build machine, so there's an updated copy of the tree at + # ~scheme/plt); the main work directory is kept the same. + if [[ -e "$installdir-backup" ]]; then _rm "$installdir-backup"; fi + if [[ -e "$installdir" ]]; then _mv "$installdir" "$installdir-backup"; fi + _mv "$installdir-new" "$installdir" + fi + fi + + ## -------------------------------------------------------------------------- + separator "${machine}(${platform}) done" + +} + +## ============================================================================ + +winpath2unix() { # input: windows path + echo "$*" | sed 's_^\([a-zA-Z]\):[/\\]_/\1/_; s_\\_/_g' +} + +build_w32step() { # inputs: type, name, [args...] + separator "Building: $2 [${machine}(${platform})] ($1)" + local btype="$1" bname="$2"; shift 2 + start_timer + case "$btype" in + ( "VSNET" ) _cd "$PLTHOME/src/worksp/$bname" + _run "$VSNET" "$bname.sln" /build "Release|Win32" + ;; + ( "NMAKE" ) _run "$NMAKE" "$@" + ;; + ( "MZCGC" ) _run "$PLTHOME/RacketCGC.exe" "$@" + ;; + ( "MZ" ) # prefer using no-suffix, then 3m, and then cgc + # (needed because cgc is used to build 3m) + local E="$PLTHOME/Racket" + if [[ -x "${E}.exe" ]]; then _run "${E}.exe" "$@" + elif [[ -x "${E}3m.exe" ]]; then _run "${E}3m.exe" "$@" + elif [[ -x "${E}CGC.exe" ]]; then _run "${E}CGC.exe" "$@" + else exit_error "No Racket executable found" + fi + ;; + ( "VSNET3M" ) _cd "$PLTHOME/src/worksp/$bname" + _run "$VSNET" "$bname.sln" /build "Release|Win32" + _run "$PLTHOME/Racket.exe" "xform.ss" "$@" + _run "$VSNET" "$bname.sln" /build "3m|Win32" + ;; + ( * ) exit_error "Unknown type for build_w32step: \"$btype\"" ;; + esac + show_time "--==> $bname on ${machine}(${platform}) done," +} + +DO_WIN32_BUILD() { + + ## -------------------------------------------------------------------------- + /usr/bin/mount -c / + + export TEMP="c:\\cygwin\\tmp" TMP="c:\\cygwin\\tmp" + + # Note: commands must be executed using unix paths (also PATH) + STUDIO="c:\\Program Files\\Microsoft Visual Studio 8" + SCOMMON="$STUDIO\\Common7" + VC="$STUDIO\\VC" + VSNET="`winpath2unix \"$SCOMMON\\IDE\\devenv.com\"`" + NMAKE="`winpath2unix \"$VC\\bin\\nmake.exe\"`" + local uSCOMMON="`winpath2unix \"$SCOMMON\"`" + local uVC="`winpath2unix \"$VC\"`" + local uPLTHOME="`winpath2unix \"$PLTHOME\"`" + PATH="$uVC/bin:$uSCOMMON/IDE:$uSCOMMON/Tools:$uSCOMMON/Tools/Bin" + PATH="$PATH:/usr/local/bin:/usr/bin:/bin" + PATH="$PATH:/c/Windows/system32:/c/Windows:/c/Windows/System32/Wbem" + PATH="$PATH:$uPLTHOME:$uPLTHOME/bin" + PATH="$PATH:." + + INCLUDE="$VC\\include;$VC\\atlmfc\\include;$VC\PlatformSDK\Include" + LIB=".;$VC\\lib;$VC\\atlmfc\\lib;$VC\\PlatformSDK\\lib" + export VSNET NMAKE PATH INCLUDE LIB + + # separator "win32: Convert .sln files" + # local SAVED_IFS="$IFS"; IFS=$'\n' + # local sln + # for sln in `find "$PLTHOME/src/worksp" -type f -name "*.sln"`; do + # _cd "`dirname \"$sln\"`" + # _run "$VSNET" /upgrade "`basename \"$sln\"`" + # done + # IFS="$SAVED_IFS" + + separator "win32: Full build" + build_w32step VSNET "mzscheme" + build_w32step VSNET "mred" + _cd "$PLTHOME/src/worksp/gc2"; build_w32step MZ "3M" make.ss + + _cd "$PLTHOME" + build_w32step VSNET "mzstart" + build_w32step VSNET "mrstart" + + separator "win32: Building libraries" + _cd "$PLTHOME"; build_w32step MZ "mzc" -l- setup -Dl compiler + + build_w32step VSNET3M "mzcom" + build_w32step VSNET3M "libmysterx" + # _cd "$PLTHOME/src/srpersist" + # build_w32step NMAKE "srpersist" /f srpersist.mak "install" + + _cd "$PLTHOME"; build_w32step MZ "setup-plt" $SETUP_ARGS + + separator "win32: Building Cygwin libreries" + _mcd "$PLTHOME/src/build" + _run ../configure --disable-mred + _cd "mzscheme/dynsrc" + show "Running \"make\" for Cygwin" + make && make cygwin-install \ + || exit_error "Errors when running \"make\" for Cygwin" + + # Borland is no longer supported: + # separator "win32: Building Borland libreries" + # _cd "$PLTHOME/src/mzscheme/dynsrc" + # _run bcc32 -I"../include" -I"g:/borland/bcc55/include" \ + # -o"mzdynb.obj" -c "mzdyn.c" + # _md "$PLTHOME/lib/bcc" + # _cp "mzdynb.obj" "mzdynb.def" "$PLTHOME/lib/bcc" + + _cd "$PLTHOME" + build_w32step MZ "winvers" -l setup/winvers; sleep 240 + +} + +## ============================================================================ + +BUILD_DOCS_AND_PDFS() { + + separator "Copying and making \"$docdir\"" + + _rmcd "$maindir/$docdir" + html_begin "Documentation" + html_content_begin + html_table_begin + { + html_file_row "html" \ + "html files for on-line browsing (same as plt/collecs/doc)" + _rm "html" + _cp -r "$workdir/$installdir/doc" "html" + } + if is_yes make_pdf_docs; then + html_file_row "pdf" "pdf versions of the manuals" + _rmcd "pdf" + # avoid any work except for the pdf generation + _run "$PLTHOME/bin/setup-plt" \ + --no-zo --no-launcher --no-install --no-post-install \ + --no-info-domain --no-docs --no-user --no-planet \ + --doc-pdf . + _cd .. + else + show "Skipping pdf build" + fi + html_table_end + html_content_end + html_end + +} + +## ============================================================================ + +COPY_AND_BUILD_BINARY_DIRS() { + + ## -------------------------------------------------------------------------- + # This creates build-related directories. The installers and + # pre-installers are built in their own steps. + + ## -------------------------------------------------------------------------- + separator "Copying and making \"$bindir\"" + + _rmcd "$maindir/$bindir" + + html_begin "Binaries" + html_content_begin + html_show "Note that the binaries include the CGC versions." + html_table_begin + + local m + for m in "${machines[@]}"; do + machine="$m" machineget mplatform=platform + mplatformname="`name_of_platform \"$mplatform\"`" + html_file_row "$mplatform" "Binaries for $mplatformname" + { + _rmcd "$mplatform" + local btgz="$installdir-$mplatform-binaries.tgz" + local ftgz="$installdir-$mplatform-full.tgz" + local prfx="" + if [[ "$m" != "$workmachine" ]]; then prfx="${m}:"; fi + _scp "${prfx}$fulltgz" "$ftgz" + local extratext="`extra_description_of_platform \"$mplatform\"`" + html_begin "$mplatformname binaries ($mplatform)" + html_content_begin + html_show "These are the $mplatformname binary files." $extratext + html_table_begin + # The following two things do not exist until the bundle script runs + html_file_row "$installdir" "The binary files part of the build tree" + html_file_row "$btgz" "An archive of the above" + html_file_row "$ftgz" "An archive of the fully-built tree" \ + "
(without the \"src\" tree)" + html_table_end + html_content_end + html_end + _cd .. + } + done + + html_table_end + html_content_end + html_end + + ## -------------------------------------------------------------------------- + separator "Making \"$stampfile\"" + + _cd "$maindir" + _rm "$stampfile" + echo "$timestamp $version" > "$stampfile" + +} + +## ============================================================================ + +BUILD_BUNDLES() { + + ## -------------------------------------------------------------------------- + # the index in this directory is made by BUILD_INSTALLERS below + + separator "Creating pre-installer bundles" + _rmd "$maindir/$preinstdir" + show "Running the bundle script" + local bundleflags="" + if [[ "$releasing" = "yes" ]]; then bundleflags="$bundleflags ++release"; fi + _run "$PLTHOME/bin/racket" \ + "$bundlescript" -o "$maindir/$preinstdir" $bundleflags + +} + +## ============================================================================ + +# platform-specific installer makers: +# $1 is input file, $2 is the output (without suffix) +# $3 is the package name (mz/plt), $4 is the type (bin/src) +# $5 is the platform name (unix/mac/win for src distributions) + +#---------------------------------------- +tgz_to_tgz() { + if [[ "$4" != "src" ]]; then + _cp "$1" "$2.tgz" + else + local savedpwd="`pwd`" + local srcdir="$3-$version" + _rmcd "$tmpdir/tgz-to-tgz-$$" + _tgunzip "$1" + _mv "$installdir" "$srcdir" + _tgzip "$2.tgz" "$srcdir" + _cd "$savedpwd" + _rm "$tmpdir/tgz-to-tgz-$$" + fi +} +#---------------------------------------- +tgz_to_sh() { + local srctgz="$1" tgtsh="$2.sh" pname="$3"; shift 3 + local tmppackdir="$tmpdir/pack-$$" + local tmptgz="$tmpdir/pack-$$.tar.gz" + local treesize installerlines archivecksum + # check paths data in configure script + if [[ "$unixpathcheckscript" != "DONE" ]]; then + show "Checking paths in configure script" + _run "$unixpathcheckscript" + unixpathcheckscript="DONE" + fi + savedpwd="`pwd`" + _rmcd "$tmppackdir" + _tgunzip "$srctgz" + _run sudo chown -R root:root "$tmppackdir" + _run sudo chmod -R g+w "$tmppackdir" + _cd "$installdir" + _run pax -w -z -f "$tmptgz" * + treesize="`get_first du -hs .`" + _cd "$savedpwd" + # change back so we can remove it + _run sudo chown -R "`id -nu`:`id -ng`" "$tmppackdir" + _rm "$tmppackdir" + archivecksum="`get_first cksum \"$tmptgz\"`" + local humanname="`name_of_dist_package \"$pname\"` v$version" + local tgtname="$pname" + if [[ "$releasing" != "yes" ]]; then tgtname="$tgtname-$version"; fi + echo "Writing \"$tgtsh\"" + { echo "#!/bin/sh" + echo "" + echo "# This is a self-extracting shell script for $humanname." + echo "# To use it, just run it, or run \"sh\" with it as an argument." + echo "" + echo "DISTNAME=\"$humanname\"" + echo "PNAME=\"$pname\"" + echo "TARGET=\"$tgtname\"" + echo "BINSUM=\"$archivecksum\"" + echo "ORIGSIZE=\"$treesize\"" + echo "RELEASED=\"$releasing\"" + } > "$tgtsh" \ + || exit_error "Could not write \"$tgtsh\"" + installerlines=$(( `get_first wc -l "$unixinstallerscript"` + + `get_first wc -l "$tgtsh"` + + 2 )) + echo "BINSTARTLINE=\"$installerlines\"" >> "$tgtsh" + cat "$unixinstallerscript" >> "$tgtsh" + cat "$tmptgz" >> "$tgtsh" + chmod +x "$tgtsh" + rm "$tmptgz" +} +#---------------------------------------- +tgz_to_zip() { + local savedpwd="`pwd`" + local srcdir="$installdir" + _rmcd "$tmpdir/tgz-to-zip-$$" + _tgunzip "$1" + if [[ "$4" = "src" ]]; then + srcdir="$3-$version" + _mv "$installdir" "$srcdir" + fi + _zip "$2.zip" "$srcdir" + _cd "$savedpwd" + _rm "$tmpdir/tgz-to-zip-$$" +} +#---------------------------------------- +make_dmg() { # inputs: dir, dmg, internet-enabled? + local srcdir="$1" tgtdmg="$2" internet_enabled="$3"; shift 3 + local tmpdmg="${tgtdmg%.dmg}-tmp.dmg" + local src="`basename \"$srcdir\"`" + local myself="`id -nu`:`id -ng`" + show "Making \"$tgtdmg\" from \"$srcdir\"" + _cd "`dirname \"$srcdir\"`" + _run sudo rm -f "$tgtdmg" "$tmpdmg" + # It should be possible to create dmgs normally, but they'd be created with + # the same user id of whoever runs this script... + _run sudo chown -R root:admin "$src" + # The following command should work fine, but it looks like hdiutil in 10.4 + # is miscalculating the needed size, making it too big in our case (and too + # small with >8GB images). It seems that it works to first generate an + # uncompressed image and then convert it to a compressed one. + # _run sudo hdiutil create -format UDZO -imagekey zlib-level=9 -ov \ + # -mode 555 -volname "$src" -srcfolder "$src" "$tgtdmg" + # so: [1] create an uncompressed image + _run sudo hdiutil create -format UDRW -ov \ + -mode 555 -volname "$src" -srcfolder "$src" "$tmpdmg" + # [2] remove the source tree + _run sudo rm -rf "$src" + # [3] create the compressed image from the uncompressed image + _run sudo hdiutil convert -format UDZO -imagekey zlib-level=9 -ov \ + "$tmpdmg" -o "$tgtdmg" + # [4] remove the uncompressed image + _run sudo chown "$myself" "$tgtdmg" "$tmpdmg" + _rm "$tmpdmg" + # this will make browsers get the dmg, mount, copy contents, unmount + if [[ "$internet_enabled" = "yes" ]]; then + _run hdiutil internet-enable "$tgtdmg" + fi +} +#---------------------------------------- +do_tgz_to_dmg() { + local internet_enabled="$1" tmptgz="$2" tmpdmg="$3" version="$4" + local packagename="$5" packagetype="$6" + shift 6 + local distname="`name_of_dist_package \"$packagename\"`" + distname="$distname v$version" + if [[ "$packagetype" != "bin" ]]; then + distname="$distname `name_of_dist_type \"$packagetype\"`" + fi + local savedpwd="`pwd`" + _rm "$tmpdmg" + _rmcd "$tmpdir/tgz-to-dmg-$$" + _mcd "$distname" + _tgunzip "$tmptgz" + _rm "$tmptgz" + _mv "$installdir" "$distname" + _cd "$tmpdir/tgz-to-dmg-$$" + make_dmg "$distname" "$tmpdmg" "$internet_enabled" + _cd "$savedpwd" + _rm "$tmpdir/tgz-to-dmg-$$" +} +tgz_to_some_dmg() { + local internet_enabled="$1" srctgz="$2" tgtdmg="$3.dmg"; shift 3 + local tmptgz="$tmpdir/tgz2dmg.tgz" + local tmpdmg="$tmpdir/tgz2dmg.dmg" + _scp "$srctgz" "${dmgmachine}:$tmptgz" + run_part "$dmgmachine" "do_tgz_to_dmg" "$internet_enabled" \ + "$tmptgz" "$tmpdmg" "$version" "$@" + _scp "${dmgmachine}:$tmpdmg" "$tgtdmg" +} +tgz_to_dmg() { + tgz_to_some_dmg "no" "$@" +} +tgz_to_idmg() { # same as ..._dmg, but makes it internet-enabled + tgz_to_some_dmg "yes" "$@" +} +#---------------------------------------- +do_tgz_to_exe() { + local tmptgz="$1" tmpexe="$2" nsistgz="$3" packagename="$4" packagetype="$5" + shift 5 + local savedpwd="`pwd`" + _rmcd "$tmpdir/tgz-to-exe-$$" + _tgunzip "$nsistgz" + _tgunzip "$tmptgz" + show "Running NSIS to create the installer" + "/c/Program Files/NSIS/makensis.exe" /V3 "plt-installer.nsi" | tr -d '\r' \ + || exit_error "NSIS build failed" + _mv "installer.exe" "$tmpexe" + _cd "$savedpwd" + _rm "$tmpdir/tgz-to-exe-$$" +} +tgz_to_exe() { + local srctgz="$1" tgtexe="$2.exe" pname="$3"; shift 3 + local nsistgz="$tmpdir/plt-nsis.tgz" + local tmptgz="$tmpdir/tgz2exe.tgz" + local tmpexe="$tmpdir/tgz2exe.exe" + _rm "$tmpdir/plt-nsis-$$" + _cp -r "$nsisdir" "$tmpdir/plt-nsis-$$" + _cd "$tmpdir/plt-nsis-$$" + show "Writing \"plt-defs.nsh\"" + { local def='!define' + echo "$def PLTVersion \"$version\"" + # this must be four numbers + echo "$def PLTVersionLong \"$version1.$version2.$version3.$version4\"" + echo "$def PLTHumanName \"`name_of_dist_package \"$pname\"` v$version\"" + if [[ "$releasing" != "yes" ]]; then + echo "$def PLTStartName \"`name_of_dist_package \"$pname\"` v$version\"" + else + echo "$def PLTStartName \"`name_of_dist_package \"$pname\"`\"" + fi + local dname + case "$pname" in + ( "plt" ) dname="PLT" ;; + ( "mz" ) dname="MzScheme" ;; + ( "full" ) dname="PLT-FULL" ;; + ( * ) exit_error "Unknown package name for exe installer: \"$pname\"" ;; + esac + if [[ "$releasing" != "yes" ]]; then + echo "$def PLTDirName \"$dname-$version\"" + else + echo "$def PLTDirName \"$dname\"" + fi + echo "$def PLTRegName \"$dname-$version\"" + if [[ "$pname" = "mz" ]]; then echo "$def SimpleInstaller"; fi + } > "plt-defs.nsh" \ + || exit_error "Could not write \"plt-defs.h\"" + local line="---------- plt-defs.nsh ----------" + echo "$line" + cat "plt-defs.nsh" + echo "$line" | sed 's/./-/g' + _tgzip "$nsistgz" * + _cd "$tmpdir" + _rm "$tmpdir/plt-nsis-$$" + _scp "$nsistgz" "${nsismachine}:$nsistgz" + _scp "$srctgz" "${nsismachine}:$tmptgz" + run_part "$nsismachine" \ + "do_tgz_to_exe" "$tmptgz" "$tmpexe" "$nsistgz" "$pname" "$@" + _scp "${nsismachine}:$tmpexe" "$tgtexe" +} +#---------------------------------------- + +do_installers_page_body() { # input: selector-html table-html + local selector="$1" table="$2"; shift 2 + local dtype dtypename dists dist distname platforms ptype ptypename + local d file fsize idx expl + local dists="plt mz full" + local dtypes="bin src" + { echo "" + echo "
" + echo "
" + echo "Distribution:   " + echo "Platform+Type:   " + echo "" + echo "
" + echo "" + echo "
" + echo "" + echo "" + } >> "$selector" + local NAcell="N/A" + local SRCcell="$cleantgz" + idx=0 + for dtype in $dtypes; do + dtypename="`name_of_dist_type \"$dtype\"`" || exit_error "Bad dist type" + echo "" >> "$table" + echo "$dtypename distributions" >> "$table" + for d in $dists; do + echo "`name_of_dist_package \"$d\"`" >> "$table" + done + echo "" >> "$table" + for ptype in `platforms_of_dist_type "$dtype"`; do + if [[ "$dtype" != "bin" || -d "$maindir/$bindir/$ptype" ]]; then + ptypename="`name_of_platform \"$ptype\"`" \ + || exit_error "Bad dist package" + echo "" >> "$table" + echo "$ptypename" >> "$table" + for d in $dists; do + if [[ "$dtype" = "src" ]]; then case "$d" in + ( "plt" | "mz" ) ;; + ( "full" ) echo "$SRCcell" >> "$table"; continue ;; + ( * ) echo "$NAcell" >> "$table"; continue ;; + esac; fi + distributions2[idx++]="$d-$dtype-$ptype" + file="`ls \"$d-$version-$dtype-$ptype.\"*`" + if [[ "$file" = "" ]]; then + echo "(missing)" >> "$table" + else + local fsize="`get_first du -h \"$file\"`" + echo "$file" >> "$table" + echo "($fsize)" >> "$table" + fi + done + echo "" >> "$table" + fi + done + done +} + +BUILD_INSTALLERS() { + + ## -------------------------------------------------------------------------- + separator "Creating platform-specific installers" + _rmd "$maindir/$instdir" + _cd "$maindir/$preinstdir" + html_begin "Pre-installers" + html_content_begin + html_table_begin + local tgz idx + idx=0 + for tgz in *.tgz; do + local dname="`basename \"$tgz\" .tgz`" + distributions1[idx++]="$dname" + local dpackage="` echo \"$dname\" | cut -d - -f 1`" + local dtype="` echo \"$dname\" | cut -d - -f 2`" + local dplatform="`echo \"$dname\" | cut -d - -f 3-`" + html_file_row "$tgz" "`name_of_dist_type \"$dtype\"` distribution of" \ + "`name_of_dist_package \"$dpackage\"` for" \ + "`name_of_platform \"$dplatform\"`" + convert="tgz_to_`installer_of_dist_type_platform \"$dtype-$dplatform\"`" + separator "Making \"$dpackage-$dtype\" installer for \"$dplatform\"" + show "Using \"$convert\" to convert \"$dname\"" + "$convert" "$maindir/$preinstdir/$tgz" \ + "$maindir/$instdir/$dpackage-$version-$dtype-$dplatform" \ + "$dpackage" "$dtype" "$dplatform" + _cd "$maindir/$preinstdir" + done + html_table_end + html_content_end + html_end + + _cd "$maindir/$instdir" + show "Making the distributions page" + _rm "$tmpdir/plt-tmp-selector" "$tmpdir/plt-tmp-table" + do_installers_page_body "$tmpdir/plt-tmp-selector" "$tmpdir/plt-tmp-table" + # selector page + html_begin "Installers" + html_content_begin + html_show -f "$tmpdir/plt-tmp-selector" + html_content_end + html_end + # static table page + html_begin "Installers (static)" "table.html" + html_content_begin + html_table_begin "all" + html_show -f "$tmpdir/plt-tmp-table" + _rm "$tmpdir/plt-tmp-selector" "$tmpdir/plt-tmp-table" + html_table_end + html_content_end + html_end + + local f sorted1 sorted2 + show "Checking generated pre-distribution and distributions on index page" + sorted1="`for f in \"${distributions1[@]}\"; do echo \"$f\"; done | sort`" + sorted2="`for f in \"${distributions2[@]}\"; do echo \"$f\"; done | sort`" + if [[ "$sorted1" = "$sorted2" ]]; then + show "File lists identical, good." + else + show "File lists do not match." + show "Generated pre-distributions:" + echo "$sorted1" + show "Indexed distributions:" + echo "$sorted2" + exit_error "Fix this script" + fi + +} + +## ============================================================================ + +move_from_maindir() { # input: file-name + if [[ -e "$maindir/$1" ]]; then + if [[ -e "$1" ]]; then _rmd "TEMP_WEB"; _mv "$1" "TEMP_WEB"; fi + _mv "$maindir/$1" . + _rm "TEMP_WEB" + elif [[ ! -e "$1" ]]; then exit_error "\"$1\" is not in $maindir or `pwd`" + else show "Skipping \"$1\"" + fi +} +copy_from() { # input: directory file-name + _rmcd "TEMP_WEB" + show "Copying: \"$1/$2\" to \"`pwd`\"" + ( cd "$1" ; tar cf - --exclude=".svn" "$2" ) | tar xf - \ + || exit_error "Could not copy \"$1/$2\" to \"`pwd`\"" + _cd ".." + if [[ -e "$2" ]]; then _mv "$2" "TEMP_WEB/TEMP_WEB"; fi + _mv "TEMP_WEB/$2" . + _rm "TEMP_WEB" +} + +BUILD_WEB() { + + local w="$prewebdir" + # cases for "hidden" results: building a major version, or a non-default path + if [[ "$reallyreleasing" = "yes" ]]; then w="$w/$version" + elif [[ "$svnpath" != "trunk" ]]; then w="$w/$svnpath" + elif [[ "$releasing" = "yes" ]]; then w="$w/$version" + fi + + ## -------------------------------------------------------------------------- + if [[ "$w" = "$prewebdir" ]]; then separator "Making external web pages" + else separator "Making external web pages at $w" + fi + + _mcd "$maindir/$w" + + html_begin "PLT Nightly Builds" + html_content_begin + html_table_begin + #---- + move_from_maindir "$installersdir" + html_file_row "$installersdir" "Installers" \ + "
(these are platform-specific distribution files, similar to" \ + "standard distributions.)" + #---- + move_from_maindir "$docdir" + html_file_row "$docdir" "Documentation files" + #---- + html_file_row "search.html" "Search the current sources and docs" + #---- + move_from_maindir "$bindir" + html_file_row "$bindir" "Platform-specific binary files" + #---- + move_from_maindir "$preinstdir" + html_file_row "$preinstdir" "Pre-installer files" \ + "
(these contain distribution files in tgz format, which are" \ + "used to create platform-specific installers)" + #---- + copy_from "$workdir" "$installdir" + html_file_row "$installdir" "A complete build tree (built on $platform)" + #---- + move_from_maindir "$cleantgz" + html_file_row "$cleantgz" \ + "The complete repository source tree packed in a gzipped tarball" + #---- + move_from_maindir "$srctgz" + html_file_row "$srctgz" "An archive containing only plt/src" \ + "
(can be used right after unpacking other tgz files to add" \ + "the src contents to an existing plt tree)" + #---- + html_file_row "script.html" "Sample scripts for using nightly builds" \ + "
(read this if you want to use automatic scripts to keep" \ + "an up-to-date installation)" + #---- + move_from_maindir "$stampfile" + html_file_row "$stampfile" "Timestamp+version file" \ + "
(updated only after a successful build, useful for" \ + "automatic scripts)" + #---- + # don't copy this, since it's still being written to + _rm "$scriptlogfile"; ln "$maindir/$scriptlogfile" "$scriptlogfile" + html_file_row "$scriptlogfile" "Full build log" + #---- + html_table_end + html_content_end + html_end + + ## -------------------------------------------------------------------------- + separator "Making and installing web content" + + _rmcd "$maindir/$webdir" + # distribute only if this is a normal build + if [[ "$w" = "$prewebdir" ]]; then + _run "$webscript" --dist + else + _run "$webscript" + fi + + ## -------------------------------------------------------------------------- + separator "Patching up pre-release web content" + + _cd "$maindir/$w" + _run "$htmlpatchscript" "$maindir/$webdir/pre" + + ## -------------------------------------------------------------------------- + if [[ "$w" = "$prewebdir" ]]; then + separator "Creating a site-map" + _cd "$maindir/$w" + _run "$sitemapdir/sitemap_gen.py" --config="$sitemapdir/plt-pre.xml" \ + > /dev/null + fi + +} + + +############################################################################### +### Main dispatch + +if [[ "$1" = "--dispatch" ]]; then + shift + while [[ "$1" = *"="* ]]; do eval "export $1"; shift; done + machine="$1"; go="$2"; shift 2 + init_svnpath_vars # set the svnpath variables according to the env vars + machineget platform # set the global platform for dependable script pieces + show "Working on $machine($hostname)" + show "Dispatching to $go($*)" + "$go" "$@" + show "Done working on $machine($hostname)" +elif [[ "$scriptlog" = "yes" ]]; then + show "Working on $machine($hostname)" + rm -f "$maindir/$scriptlogfile" + { echo "This is the build log, generated by $buildscript"; echo "" + echo "Search for \"BOOM\" for any errors."; echo "" + # set | grep "^[a-z].*=" | awk '{ print " " $0 }'; echo "" + } > "$maindir/$scriptlogfile" + if [[ "$scriptlog" = "only" ]]; then + exec >> "$maindir/$scriptlogfile" 2>&1 + MAIN "$@" + else + MAIN "$@" 2>&1 | tee -a "$maindir/$scriptlogfile" + fi +else + show "Working on $machine($hostname)" + MAIN "$@" +fi + +exit + +############################################################################### diff --git a/collects/meta/build/bundle b/collects/meta/build/bundle new file mode 100755 index 0000000000..7cfa88e925 --- /dev/null +++ b/collects/meta/build/bundle @@ -0,0 +1,562 @@ +#!/bin/env mzscheme +;; -*- scheme -*- + +#lang scheme/base + +(require scheme/cmdline scheme/runtime-path scheme/match scheme/promise + meta/checker (prefix-in dist: meta/dist-specs) meta/specs + (for-syntax scheme/base) ; for runtime-path + (except-in scheme/mpair mappend) + (only-in (lib "process.ss") system)) + +(define (/-ify x) + (regexp-replace #rx"/?$" (if (path? x) (path->string x) x) "/")) +(define home/ (/-ify (expand-user-path "~scheme"))) +(define binaries/ (/-ify (build-path home/ "binaries"))) +(define target/ (/-ify (build-path home/ "pre-installers"))) +(define plt/ (/-ify (or (getenv "PLTHOME") + (error 'bundle "PLTHOME is not defined")))) +(define plt-base/ (/-ify (simplify-path (build-path plt/ 'up) #f))) +(define plt/-name (let-values ([(base name dir?) (split-path plt/)]) + (path-element->string name))) + +(define cd current-directory) + +(define *readme-file* + (build-path plt/ "readme.txt")) +(define *info-domain-file* + (build-path plt/ "collects" "info-domain" "compiled" "cache.rktd")) + +(define *info-domain-cache* #f) + +(define-runtime-path *spec-file* "distribution-specs") +(define-runtime-path *readme-specs-file* "readme-specs") + +(define *verify?* #t) +(define *btgz?* #t) +(define *pack?* #t) +(define *root?* #t) +(define *release?* #f) +(define *verbose?* 'yes) ; #t, #f, or else -- show stderr stuff but not stdout + +;;; =========================================================================== +;;; Utilities etc + +(define concat string-append) + +(define (sort* l) + (sort l stringstring (apply directory-list args)))) + +(define (dprintf fmt . args) + (when *verbose?* + (apply fprintf (current-error-port) fmt args) + (flush-output (current-error-port)))) + +;;; =========================================================================== +;;; Tree utilities + +;; path -> tree +;; Same as get-tree, but lists the contents of a tgz file via pax. +(define (get-tgz-tree tgz) + (define base (regexp-replace #rx"/$" (path->string (cd)) "")) + (define tgz-name + (regexp-replace #rx"^.*/" (if (path? tgz) (path->string tgz) tgz) "")) + (define (tree+rest paths curdir) + (define cur-rx (regexp (concat "^" (regexp-quote curdir)))) + (define m + (let ([m (and (pair? paths) + (regexp-match-positions cur-rx (car paths)))]) + (and m (regexp-match-positions #rx"/.*/" (car paths) (cdar m))))) + (if m + ;; we have too many "/"s => need to reconstruct a fake intermediate dir + (tree+rest (cons (substring (car paths) 0 (add1 (caar m))) paths) curdir) + (let loop ([paths paths] [contents '()]) + (when (pair? paths) + (prop-set! (car paths) 'tgz tgz-name) + (prop-set! (car paths) 'base base) + (prop-set! + (car paths) 'name + (cond [(regexp-match #rx"^(?:.*/)?([^/]+)/?$" (car paths)) => cadr] + [else (error 'get-tgz-tree + "bad path name: ~s" (car paths))]))) + (if (and (pair? paths) (regexp-match? cur-rx (car paths))) + ;; still in the same subtree + (if (regexp-match? #rx"/$" (car paths)) + ;; new directory + (let-values ([(tree rest) (tree+rest (cdr paths) (car paths))]) + (loop rest (cons tree contents))) + ;; new file + (loop (cdr paths) (cons (car paths) contents))) + ;; in a new subtree + (values (cons curdir (reverse contents)) paths))))) + (define-values (p pout pin perr) + (subprocess #f /dev/null-in (current-error-port) /tar "tzf" tgz)) + (parameterize ([current-input-port pout]) + (let loop ([lines '()]) + (let ([line (read-line)]) + (if (eof-object? line) + (let ([paths (sort* (reverse lines))]) + (subprocess-wait p) + (unless (eq? 0 (subprocess-status p)) + (error 'get-tgz-tree "`tar' failed.")) + (let-values ([(tree rest) (tree+rest paths "")]) + (if (null? rest) + (cdr tree) + (error 'get-tgz-tree "something bad happened (~s...)" + (car paths))))) + (loop (cons line lines))))))) + +;;; =========================================================================== +;;; Spec management + +(define *readme-specs* (make-parameter #f)) + +;;; =========================================================================== +;;; Start working + +(register-macros!) + +(define *platforms* #f) +(define *bin-types* #f) +(define *src-types* #f) +(define *platform-tree-lists* #f) +(define /pax #f) +(define /tar #f) +(define /dev/null-out #f) +(define /dev/null-in #f) + +(define (process-command-line) + (command-line + #:multi + ["+d" "Verify dependencies (default)" (set! *verify?* #t)] + ["-d" "Don't verify dependencies" (set! *verify?* #f)] + ["+v" "Verbose mode (on stdout)" (set! *verbose?* #t)] + ["-v" "Normal output (only stderr) (default)" (set! *verbose?* 'yes)] + ["-q" "Quiet mode" (set! *verbose?* #f)] + ["+b" "Create binary tgzs (default)" (set! *btgz?* #t)] + ["-b" "Skip binary tgzs, re-use binary trees" (set! *btgz?* #f)] + ["+p" "Pack distributions (default)" (set! *pack?* #t)] + ["-p" "Skip packing" (set! *pack?* #f)] + ["+r" "chown the contents to root (default)" (set! *root?* #t)] + ["-r" "Do not chown the contents to root" (set! *root?* #f)] + ["++release" "Build for a release" (set! *release?* #t)] + ["-o" dest "Destination directory" (set! target/ (/-ify dest))] + ["--text" "Stands for -d +v -b -p -r (useful for debugging)" + (set!-values (*verify?* *verbose?* *btgz?* *pack?* *root?*) + (values #f #t #f #f #f))]) + (current-verbose-port (and *verbose?* current-error-port))) + +;; specs can have `lambda' expressions to evaluate, do it in this context +(define-namespace-anchor bundle-specs) + +(define (read-spec-file file [param *specs*]) + (process-specs + (with-input-from-file file + (lambda () + (let loop ([xs '()]) + (let ([x (read)]) + (if (eof-object? x) (reverse xs) (loop (cons x xs))))))) + param)) + +(define (read-specs) + (current-namespace (namespace-anchor->namespace bundle-specs)) + (dprintf "Reading specs...") + (dist:register-specs!) + (read-spec-file *readme-specs-file* *readme-specs*) + (dprintf " done.\n")) + +(define (input-tgz-name? f) + (let ([f (if (path? f) (path->string f) f)]) + ;; names of tgzs that are not the generated binary ones + (and (regexp-match? #rx"\\.tgz$" f) + (not (regexp-match? #rx"-binaries\\.tgz$" f))))) + +(define (initialize) + (when *release?* (*environment* (cons 'release (*environment*)))) + (set! /pax (or (find-executable-path "pax" #f) + (error "error: couldn't find a `pax' executable"))) + (set! /tar (or (find-executable-path "gtar" #f) + (error "error: couldn't find a `gtar' executable"))) + (set! /dev/null-out (open-output-file "/dev/null" #:exists 'append)) + (set! /dev/null-in (open-input-file "/dev/null")) + (unless (directory-exists? target/) (make-directory target/)) + (let ([d (ormap (lambda (x) (and (not (directory-exists? x)) x)) + (list home/ plt/ binaries/ target/))]) + (when d (error 'bundle "directory not found: ~a" d))) + (set! *platforms* + (parameterize ([cd binaries/]) + (filter (lambda (x) + (and (not (regexp-match? #rx"^[.]" x)) + (directory-exists? x))) + (dir-list)))) + (set! *bin-types* (map string->symbol *platforms*)) + (set! *src-types* + (let loop ([bins *bin-types*] [r '()]) + (if (null? bins) + (reverse r) + (let* ([bin (car bins)] [src (get-tag bin)]) + (cond + [(not src) (error 'binaries "no type assigned to `~e'" bin)] + [(not (= 1 (length src))) + (error 'binaries "bad type assignment for `~e': ~e" bin src)] + [else (loop (cdr bins) + (if (memq (car src) r) r (cons (car src) r)))]))))) + (dprintf "Scanning full tgzs") + (set! *platform-tree-lists* + (parameterize ([cd binaries/]) + (map (lambda (platform) + (dprintf ".") + (parameterize ([cd platform]) + ;; if no btgz *and* "plt" already created then use get-tree + ;; (useful when debugging stuff so re-use pre made ones) + ;; should work the same with an old tree + (if (and (directory-exists? "plt") (not *btgz?*)) + (filtered-map + (lambda (x) ; only directories contain stuff we need + (and (directory-exists? x) (get-tree x))) + (dir-list)) + (let ([trees (filtered-map + (lambda (x) + (and (file-exists? x) (input-tgz-name? x) + (get-tgz-tree x))) + (dir-list))]) + (tag (list (string->symbol platform)) + (map (lambda (tree) (tree-filter 'binaries tree)) + (apply append trees))))))) + *platforms*))) + (dprintf " done.\n") + (for-each (lambda (platform trees) + (when (null? trees) + (error 'binaries "no binaries found for ~s" platform))) + *platforms* *platform-tree-lists*) + ;; Create the readme file so it is included with the plt tree + (with-output-to-file *readme-file* newline #:exists 'truncate) + ;; Get the plt tree, remove junk and binary stuff + (set-plt-tree! plt-base/ plt/-name *platform-tree-lists*) + (set-bin-files-delayed-lists! + (delay (map (lambda (trees) + (sort* (mappend tree-flatten (add-trees trees)))) + *platform-tree-lists*))) + ;; Get the plt tree, remove junk and binary stuff + (delete-file *readme-file*)) + +;; works with any newline format, expects text that always ends with a newline, +;; does not handle tabs, does not handle prefix whitespaces, is not efficient. +(define (wrap-string str width) + (define (wrap-line str nl r) + (cond [(<= (string-length str) width) (list* nl str r)] + [(or (regexp-match-positions #rx"^.*( +)" str 0 width) + ;; no space in limit, go for the first space afterwards + (regexp-match-positions #rx"^.*?( +)" str)) + => (lambda (m) + (wrap-line (substring str (cdadr m)) nl + (list* nl (substring str 0 (caadr m)) r)))] + [else (list* nl str r)])) + (let loop ([str str] [r '()]) + (let ([m (regexp-match #rx"^(.*?)(\r\n|\r|\n)(.*)$" str)]) + (if m + (loop (cadddr m) (wrap-line (cadr m) (caddr m) r)) + (apply string-append (reverse (cons str r))))))) + +(define (make-readme) + (let ([readme (parameterize ([*specs* (*readme-specs*)]) + (apply string-append (expand-spec 'readme)))]) + (display (wrap-string readme 72)))) + +(define (make-info-domain trees) + (unless (= 1 (length trees)) + (error 'make-info-domain "got zero or multiple trees: ~e" trees)) + (let* ([collects (or (tree-filter "/plt/collects/" (car trees)) + (error 'make-info-domain "got no collects in tree"))] + [info (filter (lambda (x) + (let ([x (path->string (bytes->path (car x)))]) + (pair? (tree-filter (concat "/plt/collects/" x) + collects)))) + *info-domain-cache*)]) + (lambda () (write info) (newline)))) + +(define (create-binaries platform trees) + (parameterize ([cd (build-path binaries/ platform)]) + (let ([full-tgz (concat "plt-"platform"-full.tgz")] + [bin-tgz (concat "plt-"platform"-binaries.tgz")] + [all-tgzs (filter input-tgz-name? + (map path->string (directory-list)))]) + (unless (and (directory-exists? "plt") (not *btgz?*)) + (dprintf "Unpacking binaries in ~s ~a\n" platform all-tgzs) + ;; even if a "plt" directory exists, we just overwrite the same stuff + (unless (member full-tgz all-tgzs) + (error 'create-binaries "~a/~a not found" (cd) full-tgz)) + (for ([tgz all-tgzs]) (unpack tgz trees))) + (when *btgz?* + (dprintf "Creating ~s\n" bin-tgz) + (when (file-exists? bin-tgz) (delete-file bin-tgz)) + (let-values ([(p pout pin perr) + (subprocess + (current-output-port) /dev/null-in (current-error-port) + ;; see below for flag explanations + /pax "-w" "-x" "ustar" "-z" "-f" bin-tgz + ;; only pack the plt dir (only exception is Libraries on + ;; OSX, but that has its own dir) + "plt")]) + (subprocess-wait p)))))) + +(define (pack archive trees prefix) + ;; `pax' is used to create the tgz archives -- the main reasons for using it + ;; is the fact that it can generate portable "ustar" tar files, and that it + ;; is flexible enough to allow replacing file names, so we can collect files + ;; from different directories and make them all appear in a single one in the + ;; resulting archive. + (when (eq? #t *verbose?*) (printf "~a:\n" archive)) + (cond [*pack?* + (dprintf " packing...") + (when (file-exists? archive) (delete-file archive)) + (let*-values ([(output) (if (eq? #t *verbose?*) + (current-output-port) /dev/null-out)] + [(p pout pin perr) + ;; Note: pax prints converted paths on stderr, so + ;; silence it too unless verbose. Use only for + ;; debugging. + (subprocess + output #f output + /pax + "-w" ; write + "-x" "ustar" ; create a POSIX ustar format + "-z" ; gzip the archive + "-d" ; dont go down directories implicitly + "-s" (format ",^~a,,p" prefix) ; delete base paths + "-f" archive ; pack to this file + )]) + (parameterize ([current-output-port pin]) + (for ([t trees]) (print-tree t 'full))) + (close-output-port pin) + (subprocess-wait p) + (unless (eq? 0 (subprocess-status p)) + (error 'pack "`pax' failed.")))] + [(eq? #t *verbose?*) (for ([t trees]) (print-tree t))]) + (when (eq? #t *verbose?*) (newline)) + (flush-output)) + +(define (unpack archive trees) + ;; unpack using tar (doesn't look like there's a way to unpack according to + ;; files from stdin with pax, and it uses gnu format with @LongLinks). + (let-values + ([(p pout pin perr) + (subprocess + (current-output-port) #f (current-error-port) /tar + "x" ; extract + "-z" ; gunzip the archive + "-p" ; preserve permissions + "--files-from=-" ; read files from stdin + "-f" archive ; unpack this file + )] + [(trees) + (map (lambda (t) + (tree-filter + (lambda (t) + ;; Problem: if this returns #t/#f only, then the sources can + ;; come from multiple tgz since each file will be identified + ;; by itself. But if this is done, then no empty directories + ;; will be included (see `tree-filter' comment) and this will + ;; later be a problem (to have an empty dir in the tree but + ;; not on disk) -- so return '+ and as soon as a root is + ;; identified with the tgz, all of it will be used. + (and + (equal? archive + (prop-get (tree-path t) 'tgz + (lambda () + (error 'unpack + "no `tgz' property for ~e" t)))) + '+)) + t)) + trees)]) + (parameterize ([current-output-port pin]) + (for ([t trees]) (print-tree t 'only-files))) + (close-output-port pin) + (subprocess-wait p) + (unless (eq? 0 (subprocess-status p)) (error 'unpack "`tar' failed.")))) + +;; This code implements the binary filtering of 3m/cgc files, see +;; `binary-keep/throw-templates' in "distribution-specs.ss". +;; Careful when editing! +(define (filter-bintree tree) + (define (get-pattern spec) + (let ([rx (expand-spec spec)]) + (if (and (pair? rx) (null? (cdr rx)) (string? (car rx))) + (car rx) + (error 'filter-bintree "bad value for ~e: ~e" spec rx)))) + (define keep-pattern (get-pattern 'binary-keep)) + (define throw-pattern (get-pattern 'binary-throw)) + (define keep-rx (regexpify-spec (string-append "*" keep-pattern "*"))) + (define throw-rx (regexpify-spec (string-append "*" throw-pattern "*"))) + (define templates + (let ([ts (expand-spec 'binary-keep/throw-templates)]) + (for ([t ts]) + (unless (and (string? t) + ;; verify that it has exactly one "<...!...>" pattern + (regexp-match? #rx"^[^]*<[^]*![^]*>[^]*$" t)) + (error 'filter-bintree "bad keep/throw template: ~e" t))) + ts)) + (define (make-matcher x) ; matchers return match-positions or #f + (let ([rxs (map (lambda (t) + (let* ([x (regexp-replace #rx"!" t x)] + [x (object-name (regexpify-spec x #t))] + [x (regexp-replace #rx"<(.*)>" x "(\\1)")]) + (regexp x))) + templates)]) + (lambda (p) (ormap (lambda (rx) (regexp-match-positions rx p)) rxs)))) + (define (rassoc x l) + (and (pair? l) (if (equal? x (cdar l)) (car l) (rassoc x (cdr l))))) + (define keep? (make-matcher keep-pattern)) + (define throw? (make-matcher throw-pattern)) + (define existing-paths (tree-flatten tree)) + ;; The two `*-paths' values are association lists: (( . ) ...) + ;; both sides are unique in each list, the lhs is always an existing path + (define (find-paths pred? mode rx) + (define res '()) + (let loop ([t tree]) + (let ([p (tree-path t)]) + (cond [(pred? p) + => (lambda (m) + (let ([plain (string-append (substring p 0 (caadr m)) + (substring p (cdadr m)))]) + (when (rassoc plain res) + (error 'filter-bintree + "two ~s templates have the same plain: ~e -> ~e" + mode p plain)) + (set! res `((,p . ,plain) ,@res))) + #t)] + [(regexp-match? rx p) + ;; other matches are not allowed, unless on a directory where + ;; all files are selected + (when (or (not (pair? t)) + (memq #f (map loop (cdr t)))) + (error 'filter-bintree + "~s path uncovered by patterns: ~e" mode p)) + #t] + [(pair? t) (not (memq #f (map loop (cdr t))))] + [else #f]))) + res) + (define keep-paths (find-paths keep? 'keep keep-rx)) + (define throw-paths (find-paths throw? 'throw throw-rx)) + (for ([k keep-paths]) + (when (assoc (car k) throw-paths) + (error 'filter-bintree + "a path matched both keep and throw patterns: ~s" (car k)))) + (let* ([ps (map cdr keep-paths)] + [ps (append ps (remove* ps (map cdr throw-paths)))] + [scan (lambda (f paths) + (map (lambda (p) (cond [(f p paths) => car] [else #f])) ps))] + [plain (scan member existing-paths)] + [keep (scan rassoc keep-paths)] + [throw (scan rassoc throw-paths)]) + (define del + (map (lambda (p k t) + (cond + [(and p k t) (error 'filter-bintree "got keep+throw+plain")] + [(or k t) (or t p)] + [else (error 'filter-bintree "internal error")])) + plain keep throw)) + (tree-filter `(not (or ,(lambda (t) (and (memq (tree-path t) del) '+)) + binary-throw-more)) + tree))) + +;; This is hooked below as a `distribute!' spec macro, and invoked through +;; expand-spec. +(define (distribute!) + (define (distribute tree) (tree-filter 'distribution tree)) + (let* ([features (filter string? (reverse (*environment*)))] + [name (apply concat (cdr (mappend (lambda (x) (list "-" x)) + features)))] + [features (map string->symbol features)] + [bin? (memq 'bin features)] + [src? (memq 'src features)] + [full? (memq 'full features)]) + (when (and bin? src?) + (error 'distribute! "bad configuration (both bin & src): ~e" features)) + (unless (or bin? src?) + (error 'distribute! "bad configuration (both bin & src): ~e" features)) + (for ([type (if bin? *bin-types* *src-types*)] + ;; this is unused if bin? is false + [bin-trees (if bin? *platform-tree-lists* *src-types*)]) + (tag (cons type features) + (let ([name (format "~a-~a.tgz" name type)]) + (dprintf "Creating ~s: filtering..." name) + (let ([trees (add-trees + (cons (distribute (get-plt-tree)) + (if bin? + (tag 'in-binary-tree + (map (if full? + distribute + (lambda (t) + (distribute (filter-bintree t)))) + bin-trees)) + '())))]) + ;; make it possible to write these files + (chown 'me *readme-file* *info-domain-file*) + (with-output-to-file *readme-file* #:exists 'truncate make-readme) + (with-output-to-file *info-domain-file* #:exists 'truncate + (make-info-domain trees)) + (chown 'root *readme-file* *info-domain-file*) + (pack (concat target/ name) trees + (if bin? + (format "\\(~a\\|~a~a/\\)" plt-base/ binaries/ type) + plt-base/))) + (dprintf " done.\n"))))) + '()) +(register-spec! 'distribute! + (lambda () (when (or *pack?* (eq? #t *verbose?*)) (distribute!)))) + +(register-spec! 'verify! (lambda () (when *verify?* (verify!)))) + +;; make auto-generated files exist +(define (create-generated-files) + ;; no need to create the cache.ss, since it's there, but read it + (set! *info-domain-cache* + (with-input-from-file *info-domain-file* read)) + (with-output-to-file *readme-file* newline #:exists 'truncate)) +(define (delete-generated-files) + ;; don't delete the cache, but write original unfiltered contents + (with-output-to-file *info-domain-file* + (lambda () (write *info-domain-cache*) (newline)) #:exists 'truncate) + (delete-file *readme-file*)) + +;; mimic the chown syntax +(define (chown #:rec [rec #f] who path . paths) + (when (and *root?* *pack?*) + (let ([user:group + (case who [(root) "root:root"] [(me) (force whoami)] + [else (error 'chown "unknown user spec: ~e" who)])] + [paths (map (lambda (x) (if (path? x) (path->string x) x)) + (cons path paths))]) + (when (ormap (lambda (x) (regexp-match? #rx"[^/a-zA-Z0-9_ .+-]" x)) paths) + (error 'chown "got a path that needs shell-quoting: ~a" paths)) + (system (format "sudo chown ~a ~a ~a" (if rec "-R" "") user:group + (apply string-append + (map (lambda (p) (format " \"~a\"" p)) paths))))))) + +(define whoami + (delay + (parameterize ([current-output-port (open-output-string)]) + (system "echo \"`id -nu`:`id -ng`\"") + (regexp-replace + #rx"[ \r\n]*$" (get-output-string (current-output-port)) "")))) + +(define (chown-dirs-to who) + (when (and *root?* *pack?*) + (dprintf "Changing owner to ~a..." who) + (for ([dir (list plt/ binaries/)]) + (parameterize ([cd dir]) (chown #:rec #t who "."))) + (dprintf " done.\n"))) + +(process-command-line) +(read-specs) +(initialize) +(for-each create-binaries *platforms* *platform-tree-lists*) +(dynamic-wind + (lambda () (create-generated-files) (chown-dirs-to 'root)) + ;; Start the verification and distribution + (lambda () (expand-spec 'distributions) (void)) + (lambda () (chown-dirs-to 'me) (delete-generated-files))) diff --git a/collects/meta/build/info.rkt b/collects/meta/build/info.rkt new file mode 100644 index 0000000000..dd5e03927c --- /dev/null +++ b/collects/meta/build/info.rkt @@ -0,0 +1,2 @@ +#lang setup/infotab +(define compile-omit-paths 'all) diff --git a/collects/meta/build/make-patch b/collects/meta/build/make-patch new file mode 100755 index 0000000000..ea8bba724d --- /dev/null +++ b/collects/meta/build/make-patch @@ -0,0 +1,200 @@ +#!/bin/sh +#| -*- scheme -*- +exec racket "$0" + +Instructions: + +* Create a copy of a distributed PLT tree, change all files that need to change + for the patch. If this is not a first patch, then begin this process with a + tree that has the previous patch applied. (Patch numbers should go from 1 + up.) + + I do this: + cd + svn co http://svn.plt-scheme.org/plt/tags/ patched + cd patched + svn merge -r: http://svn.plt-scheme.org/plt/trunk + ... more merges as needed ... + +* Make sure that "collects/version/patchlevel.ss" contains the new patch + number, and add comments about this patch, with a list of files that are + modified. (This is good for the next step, when doing additional patches.) + +* In the code below, + - set `plt-version' to the version you're patching (base version, the code + will expect `(version)' to return an equal value). + - set `plt-base' to the location of the patched PLT tree on your system. + - put the list of files in the `files' definition. Each patch should also + have all preceding patches in it, which means that if you're patching an + already-patched tree, then you should add more files. (This is why it is + good to keep track of the modified files.) Note that + "collects/version/patchlevel.ss" must be included in this list, and that + the file does have the correct patchlevel number (there is currently no way + to check whether the patchlevel makes sense). + +* Note that the patch is a collection with the same name ("plt-patch" below). + This means that installing a patch is a process that first overwrites any + preexisting patch collections. This is fine, because patches are linear and + cumulative. The worst that can happen is that someone downloads a patch + older than what's installed -- in that case the PLT tree already has the + higher patch level, and when the collection's installer is doing its work it + will simply be skipped (a successful patch installation happens only once, + and is later skipped when setup-plt is re-run). + +* Test, put in "iplt/web/download/patches/", publish new html, announce. + +* Commit the patched tree as a new tag. + +|# + +#lang mzscheme + +;; ============================================================================ +;; customization (items marked with `[*]' should be edited for all patches) + +;; [*] which PLT version is this patch for? +(define plt-version "370") + +;; [*] location of a patched PLT tree +(define plt-base "~/patched") + +;; [*] patched files in this tree (including previously patched files, if any) +(define files '("collects/version/patchlevel.ss" + "collects/drscheme/private/module-language.ss" + "collects/framework/private/scheme.ss" + "collects/slideshow/tool.ss" + "collects/lang/htdp-langs.ss" + "collects/drscheme/private/unit.ss")) + +;; message to show after the last `Done' (#f => no extra text) +(define exit-message "please restart DrScheme") + +;; template for the output archive file +(define patchfile-template "/tmp/plt-patch-v~ap~a.plt") + +;; template for archive name +(define name-template "PLT Scheme v~ap~a patch") + +;; patchlevel file in the PLT tree (must be included in `files' above) +(define patchlevel-file "collects/version/patchlevel.ss") + +;; ============================================================================ +;; code folows + +(require (lib "list.ss") (lib "pack.ss" "setup")) + +;; move patchlevel file to the end +(unless (member patchlevel-file files) + (error 'make-patch + "missing patchlevel file (~a) in the list of files" patchlevel-file)) +(set! files (append (remove patchlevel-file files) (list patchlevel-file))) + +(unless (absolute-path? plt-base) + (error 'make-patch "plt-base is not an absolute path: ~a" plt-base)) + +(define patchlevel + ;; use `dynamic-require' -- not `require' since the patch can be built by a + ;; different PLT installation + (dynamic-require (build-path plt-base patchlevel-file) 'patchlevel)) +(define archive-name (format name-template plt-version patchlevel)) +(define archive-filename (format patchfile-template plt-version patchlevel)) + +(define unpacker-body + `((define me ,(format "v~ap~a-patch" plt-version patchlevel)) + (define (error* fmt . args) + (error (string-append "ERROR applying "me": " (apply format fmt args)))) + (define (message fmt . args) + (printf "*** ~a: ~a\n" me (apply format fmt args))) + (define collects-dir (find-collects-dir)) + (cond + [(not (equal? ,plt-version (version))) + (error* "bad version number; this patch is for version ~a, you have ~a" + ',plt-version (version))] + [(= patchlevel ,patchlevel) (error* "Already installed")] + [(> patchlevel ,patchlevel) (error* "Newer patch installed")] + [else (message "Applying patch...")]) + (mzuntar void) + (message "Patch applied successfully, recompiling...") + ;; return a list of all toplevel collections to recompile + ;; (define (has-info? c) + ;; (file-exists? (build-path collects-dir c "info.ss"))) + ;; (let* ([cs (directory-list collects-dir)] + ;; [cs (filter has-info? cs)] + ;; [cs (map path->string cs)] + ;; [cs (sort cs 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 literal 0 HcmV?d00001 diff --git a/collects/meta/build/nsis/plt-header.bmp b/collects/meta/build/nsis/plt-header.bmp new file mode 100644 index 0000000000000000000000000000000000000000..094549bde4bd165fc2ef7e4fe85fb610ff7dfe2b GIT binary patch literal 25818 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 ax1FoscCLQg-$rQr+X!uc-{RB1Z}ESK1-G&Q literal 0 HcmV?d00001 diff --git a/collects/meta/build/nsis/plt-installer.ico b/collects/meta/build/nsis/plt-installer.ico new file mode 100644 index 0000000000000000000000000000000000000000..632f0551f0d838c064e476538ccc09c4712e2fbb GIT binary patch literal 25214 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�$^4h(0(mybsp7bw|f~7Ao?*kZ4cHb%jGg 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 M^nY_#)lP{21CW@9Jpcdz literal 0 HcmV?d00001 diff --git a/collects/meta/build/nsis/plt-installer.nsi b/collects/meta/build/nsis/plt-installer.nsi new file mode 100644 index 0000000000..7adf687b72 --- /dev/null +++ b/collects/meta/build/nsis/plt-installer.nsi @@ -0,0 +1,305 @@ +!include "MUI2.nsh" +!include "WinVer.nsh" +!include "nsDialogs.nsh" + +;; ==================== Configuration + +;; The following should define: +;; PLTVersion, PLTVersionLong, PLTHumanName, +;; PLTDirName, PLTRegName + +!include plt-defs.nsh + +Name "${PLTHumanName}" +OutFile "installer.exe" + +BrandingText "${PLTHumanName}" +BGGradient 4040A0 101020 + +SetCompressor /SOLID "LZMA" + +InstallDir "$PROGRAMFILES\${PLTDirName}" +!ifndef SimpleInstaller + InstallDirRegKey HKLM "Software\${PLTRegName}" "" +!endif +!define MUI_STARTMENUPAGE_DEFAULTFOLDER "${PLTStartName}" +!define MUI_ICON "plt-installer.ico" +!define MUI_UNICON "plt-uninstaller.ico" +!define MUI_HEADERIMAGE +!define MUI_HEADERIMAGE_BITMAP "plt-header.bmp" +!define MUI_HEADERIMAGE_BITMAP_RTL "plt-header-r.bmp" +!define MUI_HEADERIMAGE_RIGHT + +!define MUI_WELCOMEFINISHPAGE_BITMAP "plt-welcome.bmp" +!define MUI_UNWELCOMEFINISHPAGE_BITMAP "plt-welcome.bmp" + +!define MUI_WELCOMEPAGE_TITLE "${PLTHumanName} Setup" +!define MUI_UNWELCOMEPAGE_TITLE "${PLTHumanName} Uninstall" +!ifdef SimpleInstaller + !define MUI_WELCOMEPAGE_TEXT "This is a simple installer for ${PLTHumanName}.$\r$\n$\r$\nIt will only create the PLT folder. To uninstall, simply remove the folder.$\r$\n$\r$\n$_CLICK" +!else + !define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of ${PLTHumanName}.$\r$\n$\r$\nPlease close other PLT applications (DrScheme, MrEd, MzScheme) so the installer can update relevant system files.$\r$\n$\r$\n$_CLICK" +!endif +!define MUI_UNWELCOMEPAGE_TEXT "This wizard will guide you through the removal of ${PLTHumanName}.$\r$\n$\r$\nBefore starting, make sure PLT applications (DrScheme, MrEd, MzScheme) are not running.$\r$\n$\r$\n$_CLICK" + +!define MUI_FINISHPAGE_TITLE "${PLTHumanName}" +!ifdef SimpleInstaller + !define MUI_FINISHPAGE_RUN + !define MUI_FINISHPAGE_RUN_FUNCTION OpenInstDir + Function OpenInstDir + ExecShell "" "$INSTDIR" + FunctionEnd + !define MUI_FINISHPAGE_RUN_TEXT "Open the installation folder" +!else + !define MUI_FINISHPAGE_RUN "$INSTDIR\DrScheme.exe" + !define MUI_FINISHPAGE_RUN_TEXT "Run DrScheme" +!endif +!define MUI_FINISHPAGE_LINK "Visit the PLT Scheme web site" +!define MUI_FINISHPAGE_LINK_LOCATION "http://www.plt-scheme.org/" + +; !define MUI_UNFINISHPAGE_NOAUTOCLOSE ; to allow users see what was erased + +!define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKLM" +!define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\${PLTRegName}" +!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" + +; Doesn't work on some non-xp machines +; !define MUI_INSTFILESPAGE_PROGRESSBAR colored + +VIProductVersion "${PLTVersionLong}" +VIAddVersionKey "ProductName" "PLT Scheme" +VIAddVersionKey "Comments" "This is PLT Scheme, including DrScheme which is based on MrEd and MzScheme." +VIAddVersionKey "CompanyName" "PLT" +VIAddVersionKey "LegalCopyright" "© PLT" +VIAddVersionKey "FileDescription" "PLT Scheme Installer" +VIAddVersionKey "FileVersion" "${PLTVersion}" + +;; ==================== Variables + +!ifndef SimpleInstaller + Var MUI_TEMP + Var STARTMENU_FOLDER +!endif + +;; ==================== Interface + +!define MUI_ABORTWARNING + +; Install +!insertmacro MUI_PAGE_WELCOME +!define MUI_PAGE_CUSTOMFUNCTION_LEAVE myTestInstDir +!insertmacro MUI_PAGE_DIRECTORY +!ifndef SimpleInstaller + !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER +!endif +!insertmacro MUI_PAGE_INSTFILES + +; Uncheck and hide the "run" checkbox on vista, since it will run with +; elevated permissions (see also ../nsis-vista-note.txt) +!define MUI_PAGE_CUSTOMFUNCTION_SHOW DisableRunCheckBoxIfOnVista +!insertmacro MUI_PAGE_FINISH +Function DisableRunCheckBoxIfOnVista + ${If} ${AtLeastWinVista} + ; use EnableWindow instead of ShowWindow to just disable it + ShowWindow $mui.FinishPage.Run 0 + ${NSD_Uncheck} $mui.FinishPage.Run + ${EndIf} +FunctionEnd + +!ifndef SimpleInstaller + ; Uninstall + !define MUI_WELCOMEPAGE_TITLE "${MUI_UNWELCOMEPAGE_TITLE}" + !define MUI_WELCOMEPAGE_TEXT "${MUI_UNWELCOMEPAGE_TEXT}" + ; !insertmacro MUI_UNPAGE_WELCOME + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + ; !insertmacro MUI_UNPAGE_FINISH +!endif + +!ifndef SimpleInstaller + !define MUI_CUSTOMFUNCTION_UNGUIINIT un.myGUIInit +!endif + +!insertmacro MUI_LANGUAGE "English" + +!ifndef SimpleInstaller + !define UNINSTEXE "$INSTDIR\Uninstall.exe" +!endif + +;; ==================== Installer + +!ifdef SimpleInstaller +Function myTestInstDir + IfFileExists "$INSTDIR\*.*" +1 inst_dir_exists + MessageBox MB_YESNO "The directory '$INSTDIR' already exists, continue?" /SD IDYES IDYES inst_dir_exists + Abort + inst_dir_exists: +FunctionEnd +!else +Function myTestInstDir + ; The assumption is that users might have all kinds of ways to get a PLT + ; tree, plus, they might have an old wise-based installation, so it is better + ; to rely on files rather than test registry keys. Note: no version check. + ; if any of these exist, then we assume it's an old installation + IfFileExists "$INSTDIR\MzScheme.exe" plt_is_installed + IfFileExists "$INSTDIR\MrEd.exe" plt_is_installed + IfFileExists "$INSTDIR\DrScheme.exe" plt_is_installed + IfFileExists "$INSTDIR\collects" plt_is_installed + Goto plt_is_not_installed + plt_is_installed: + IfFileExists "${UNINSTEXE}" we_have_uninstall + MessageBox MB_YESNO "It appears that there is an existing PLT Scheme installation in '$INSTDIR', but no Uninstaller was found.$\r$\nContinue anyway (not recommended)?" /SD IDYES IDYES maybe_remove_tree + Abort + we_have_uninstall: + MessageBox MB_YESNO "It appears that there is an existing PLT Scheme installation in '$INSTDIR'.$\r$\nDo you want to uninstall it first (recommended)?" /SD IDNO IDNO maybe_remove_tree + HideWindow + ClearErrors + ExecWait '"${UNINSTEXE}" _?=$INSTDIR' + IfErrors uninstaller_problematic + IfFileExists "$INSTDIR\MzScheme.exe" uninstaller_problematic + IfFileExists "$INSTDIR\MrEd.exe" uninstaller_problematic + BringToFront + Goto plt_is_not_installed + uninstaller_problematic: + MessageBox MB_YESNO "Errors in uninstallation!$\r$\nDo you want to quit and sort things out now (highly recommended)?" /SD IDNO IDNO maybe_remove_tree + Quit + maybe_remove_tree: + MessageBox MB_YESNO "Since you insist, do you want to simply remove the previous directory now?$\r$\n(It is really better if you sort this out manually.)" /SD IDYES IDNO plt_is_not_installed + RMDir /r $INSTDIR + plt_is_not_installed: +FunctionEnd +!endif + +Section "" + SetShellVarContext all + + SetDetailsPrint both + DetailPrint "Installing PLT Scheme..." + SetDetailsPrint listonly + SetOutPath "$INSTDIR" + File /a /r "plt\*.*" + !ifndef SimpleInstaller + WriteUninstaller "${UNINSTEXE}" ; Create uninstaller + !endif + + !ifndef SimpleInstaller + SetDetailsPrint both + DetailPrint "Creating Shortcuts..." + SetDetailsPrint listonly + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + SetOutPath "$INSTDIR" ; Make installed links run in INSTDIR + CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\DrScheme.lnk" "$INSTDIR\DrScheme.exe" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\PLT Documentation.lnk" "$INSTDIR\plt-help.exe" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\MrEd.lnk" "$INSTDIR\MrEd.exe" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\MzScheme.lnk" "$INSTDIR\MzScheme.exe" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\PLT Folder.lnk" "$INSTDIR" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "${UNINSTEXE}" + !insertmacro MUI_STARTMENU_WRITE_END + + SetDetailsPrint both + DetailPrint "Setting Registry Keys..." + SetDetailsPrint listonly + WriteRegStr HKLM "Software\${PLTRegName}" "" "$INSTDIR" ; Save folder location + WriteRegStr HKCR ".ss" "" "Scheme.Document" + WriteRegStr HKCR ".scm" "" "Scheme.Document" + WriteRegStr HKCR ".scrbl" "" "Scheme.Document" + WriteRegStr HKCR "Scheme.Document" "" "PLT Scheme Document" + WriteRegStr HKCR "Scheme.Document\DefaultIcon" "" "$INSTDIR\collects\icons\schemedoc.ico" + WriteRegStr HKCR "Scheme.Document\shell\open\command" "" '"$INSTDIR\DrScheme.exe" "%1"' + ; Example, in case we want some things like this in the future + ; WriteRegStr HKCR "Scheme.Document\shell\mzscheme" "" "Run with MzScheme" + ; WriteRegStr HKCR "Scheme.Document\shell\mzscheme\command" "" '"$INSTDIR\MzScheme.exe" "-r" "%1"' + WriteRegStr HKCR ".plt" "" "Setup PLT.Document" + WriteRegStr HKCR "Setup PLT.Document" "" "PLT Scheme Package" + WriteRegStr HKCR "Setup PLT.Document\DefaultIcon" "" "$INSTDIR\collects\icons\schemedoc.ico" + WriteRegStr HKCR "Setup PLT.Document\shell\open\command" "" '"$INSTDIR\Setup PLT.exe" -p "%1"' + + WriteRegExpandStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "UninstallString" '"${UNINSTEXE}"' + WriteRegExpandStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "InstallLocation" "$INSTDIR" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "DisplayName" "${PLTHumanName}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "DisplayIcon" "$INSTDIR\DrScheme.exe,0" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "DisplayVersion" "${PLTVersion}" + ; used to also have "VersionMajor" & "VersionMinor" but looks like it's not needed + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "HelpLink" "http://www.plt-scheme.org/" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "URLInfoAbout" "http://www.plt-scheme.org/" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "Publisher" "PLT Scheme Inc." + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "NoModify" "1" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" "NoRepair" "1" + !endif + + SetDetailsPrint both + DetailPrint "Installation complete." +SectionEnd + +;; ==================== Uninstaller + +!ifndef SimpleInstaller + +Function un.myGUIInit + ; if any of these exist, then we're fine + IfFileExists "$INSTDIR\MzScheme.exe" plt_is_installed_un + IfFileExists "$INSTDIR\MrEd.exe" plt_is_installed_un + IfFileExists "$INSTDIR\DrScheme.exe" plt_is_installed_un + IfFileExists "$INSTDIR\collects" plt_is_installed_un + MessageBox MB_YESNO "It does not appear that PLT Scheme is installed in '$INSTDIR'.$\r$\nContinue anyway (not recommended)?" /SD IDYES IDYES plt_is_installed_un + Abort "Uninstall aborted by user" + plt_is_installed_un: +FunctionEnd + +Section "Uninstall" + SetShellVarContext all + + SetDetailsPrint both + DetailPrint "Removing the PLT Scheme installation..." + SetDetailsPrint listonly + Delete "$INSTDIR\*.exe" + Delete "$INSTDIR\README*.*" + RMDir /r "$INSTDIR\collects" + RMDir /r "$INSTDIR\include" + RMDir /r "$INSTDIR\lib" + RMDir /r "$INSTDIR\doc" + ;; these exist in PLT-Full installations + RMDir /r "$INSTDIR\man" + RMDir /r "$INSTDIR\src" + Delete "${UNINSTEXE}" + RMDir "$INSTDIR" + ;; if the directory is opened, it will take some time to remove + Sleep 1000 + IfErrors +1 uninstall_inst_dir_ok + MessageBox MB_YESNO "The PLT Scheme installation at '$INSTDIR' was not completely removed.$\r$\nForce deletion?$\r$\n(Make sure no PLT applications are running.)" /SD IDYES IDNO uninstall_inst_dir_ok + RMDir /r "$INSTDIR" + IfErrors +1 uninstall_inst_dir_ok + MessageBox MB_OK "Forced deletion did not work either, you will need to clean up '$INSTDIR' manually." /SD IDOK + uninstall_inst_dir_ok: + + SetDetailsPrint both + DetailPrint "Removing Shortcuts..." + SetDetailsPrint listonly + !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP + Delete "$SMPROGRAMS\$MUI_TEMP\*.lnk" + ;; Delete empty start menu parent diretories + StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" + startMenuDeleteLoop: + RMDir $MUI_TEMP + GetFullPathName $MUI_TEMP "$MUI_TEMP\.." + IfErrors startMenuDeleteLoopDone + StrCmp $MUI_TEMP $SMPROGRAMS startMenuDeleteLoopDone startMenuDeleteLoop + startMenuDeleteLoopDone: + + SetDetailsPrint both + DetailPrint "Removing Registry Keys..." + SetDetailsPrint listonly + DeleteRegKey /ifempty HKLM "Software\${PLTRegName}\Start Menu Folder" + DeleteRegKey /ifempty HKLM "Software\${PLTRegName}" + DeleteRegKey HKCR ".ss" + DeleteRegKey HKCR ".scm" + DeleteRegKey HKCR ".scrbl" + DeleteRegKey HKCR "Scheme.Document" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PLTRegName}" + + SetDetailsPrint both + DetailPrint "Uninstallation complete." +SectionEnd + +!endif diff --git a/collects/meta/build/nsis/plt-uninstaller.ico b/collects/meta/build/nsis/plt-uninstaller.ico new file mode 100644 index 0000000000000000000000000000000000000000..2b2e20e2c00f43bcf82eeea7099bf075f749a2b3 GIT binary patch literal 25214 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 Y65j^&%qk;_;4`Fmtaz({>b5fe4*z>% literal 0 HcmV?d00001 diff --git a/collects/meta/build/nsis/plt-welcome.bmp b/collects/meta/build/nsis/plt-welcome.bmp new file mode 100644 index 0000000000000000000000000000000000000000..dbcb5ff482b366b76d1135a48f51281817f85dad GIT binary patch literal 154542 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

tJv& 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 UX_H@^aA3lL2?r(|sEq^v0SP5XWB>pF literal 0 HcmV?d00001 diff --git a/collects/meta/build/patch-html b/collects/meta/build/patch-html new file mode 100755 index 0000000000..2e66f63da7 --- /dev/null +++ b/collects/meta/build/patch-html @@ -0,0 +1,93 @@ +#!/bin/sh +#| -*- mode: scheme -*- +if [ -x "$PLTHOME/bin/mzscheme" ]; then + exec "$PLTHOME/bin/mzscheme" -rm "$0" "$@" +else + exec "mzscheme" -rm "$0" "$@" +fi +|# + +(define begin-pattern #"\n") +(define end-pattern #"\n") + +(define begin-re (regexp-replace #"XXX" begin-pattern #"([^<> ]+)")) +(define end-re (regexp-replace #"XXX" end-pattern #"([^<> ]+)")) + +(define (regexp-match1 rx inp . disp?) + (cond [(if (and (pair? disp?) (car disp?)) + (regexp-match rx inp 0 #f (current-output-port)) + (regexp-match rx inp)) + => cadr] + [else #f])) + +(define (eprintf fmt . args) + (apply fprintf (current-error-port) fmt args)) + +(define (patch-file skeleton html) + (let ([skeleton (open-input-file skeleton)] + [html (open-input-file html)]) + (let loop () + (let ([begin-tag (regexp-match1 begin-re skeleton #t)]) + ;; (eprintf ">>> skeleton: ~a begin\n" begin-tag) + (if begin-tag + (let ([begin-tag* (regexp-match1 begin-re html)]) + ;; (eprintf ">>> html: ~a begin\n" begin-tag*) + (unless (equal? begin-tag begin-tag*) + (error 'patch-html + "mismatched input begin-tags, expecting ~a got ~a" + begin-tag begin-tag*)) + ;; leave tags in, so it is possible to run this script again + (display (regexp-replace #"XXX" begin-pattern begin-tag)) + (let ([end-tag (regexp-match1 end-re html #t)]) + ;; (eprintf ">>> html: ~a end\n" end-tag) + (unless (equal? end-tag begin-tag) + (error 'patch-html "bad end tag (~a) for begin tag (~a)" + end-tag begin-tag)) + (let ([end-tag* (regexp-match1 end-re skeleton)]) + ;; (eprintf ">>> skeleton: ~a end\n" end-tag*) + (unless (equal? end-tag end-tag*) + (error 'patch-html + "mismatched input end-tags, expecting ~a got ~a" + end-tag end-tag*)) + ;; leave tags in, so it is possible to run this script again + (display (regexp-replace #"XXX" end-pattern end-tag)) + (loop)))) + (cond [(regexp-match1 begin-re html) => + (lambda (tag) + (error 'patch-html + "mismatched input tags, extraneous tag in target: ~a" + tag))])))) + (close-input-port skeleton) + (close-input-port html))) + +(define (patch-dir skeleton-dir) + (printf "patching directory: ~a\n" (current-directory)) + (for-each (lambda (p) + (if (cdr p) + (begin + (unless (directory-exists? (car p)) (make-directory (car p))) + (parameterize ([current-directory (car p)]) + (patch-dir (build-path skeleton-dir (car p))))) + (let ([skeleton (build-path skeleton-dir (car p))]) + (if (file-exists? (car p)) + (let ([tmp "/tmp/patch-html-file"]) + (printf "patching file: ~a\n" + (build-path (current-directory) (car p))) + (with-output-to-file tmp + (lambda () (patch-file skeleton (car p))) + #:exists 'truncate) + (delete-file (car p)) + (copy-file tmp (car p)) + (delete-file tmp)) + (begin (printf "copying file: ~a/~a\n" + (current-directory) (car p)) + (copy-file skeleton (car p))))))) + (parameterize ([current-directory skeleton-dir]) + (map (lambda (p) + (cons p (cond [(file-exists? p) #f] + [(directory-exists? p) #t] + [else (error "internal-error")]))) + (directory-list))))) + +(define (main arg) + (patch-dir (path->complete-path arg))) diff --git a/collects/meta/build/readme-specs b/collects/meta/build/readme-specs new file mode 100644 index 0000000000..bc73068d32 --- /dev/null +++ b/collects/meta/build/readme-specs @@ -0,0 +1,137 @@ +;; -*- scheme -*- + +;; This file defines the readme files for the different distributions. It is +;; similar to the distribution specs file, see that for explanations on its +;; format. + +\\ := (cond win => "\r\n" + ;; (or ppc-osx-mac i386-osx-mac) => "\r" ; is this still needed? + else => "\n" ) + +package-name +:= (cond full => "PLT Scheme Full Repository" + plt => "PLT Scheme" + dr => "DrScheme" + mr => "MrEd" + mz => "MzScheme") + +dist-type +:= (cond src => "source" + else => "executable") + +platform-type +:= (cond unix => "Unix" + mac => "Macintosh" + win => "Windows") +platform +:= (cond i386-linux => "Linux (i386)" + i386-linux-gcc2 => "Linux (i386/gcc2)" + i386-linux-fc2 => "Fedora Core 2 (i386)" + i386-linux-fc5 => "Fedora Core 5 (i386)" + i386-linux-fc6 => "Fedora Core 6 (i386)" + i386-linux-f7 => "Fedora 7 (i386)" + x86_64-linux-f7 => "Fedora 7 (x86_64)" + i386-linux-f9 => "Fedora 9 (i386)" + i386-linux-f12 => "Fedora 12 (i386)" + i386-linux-debian => "Debian Stable (i386)" + i386-linux-debian-testing => "Debian Testing (i386)" + i386-linux-debian-unstable => "Debian Unstable (i386)" + i386-linux-ubuntu => "Ubuntu (i386)" + i386-linux-ubuntu-dapper => "Ubuntu Dapper (i386)" + i386-linux-ubuntu-edgy => "Ubuntu Edgy (i386)" + i386-linux-ubuntu-feisty => "Ubuntu Feisty (i386)" + i386-linux-ubuntu-hardy => "Ubuntu Hardy (i386)" + i386-linux-ubuntu-intrepid => "Ubuntu Intrepid (i386)" + i386-linux-ubuntu-jaunty => "Ubuntu Jaunty (i386)" + i386-freebsd => "FreeBSD (i386)" + sparc-solaris => "Solaris" + ppc-osx-mac => "Mac OS X (PPC)" + i386-osx-mac => "Mac OS X (Intel)" + ppc-darwin => "Mac OS X using X11 (PPC)" + i386-darwin => "Mac OS X using X11 (Intel)" + i386-win32 => "Windows" + else => platform-type) + +executable := (cond mac => "application" else => "executable") +dir := (cond (or win mac) => "folder" else => "directory") + +version := (lambda () (version)) + +drscheme* +:= (cond unix => "bin/drscheme" win => "DrScheme.exe" mac => "DrScheme") +plt-help* +:= (cond unix => "bin/plt-help" win => "plt-help.exe" mac => "bin/plt-help") +setup-plt* +:= (cond unix => "bin/setup-plt" win => "Setup PLT.exe" mac => "bin/setup-plt") +mred* +:= (cond unix => "bin/mred" win => "MrEd.exe" mac => "MrEd") +mzscheme* +:= (cond unix => "bin/mzscheme" win => "MzScheme.exe" mac => "bin/mzscheme") +mzc* +:= (cond unix => "bin/mzc" win => "mzc.exe" mac => "bin/mzc") +planet* +:= (cond unix => "bin/planet" win => "planet.exe" mac => "bin/planet") + +intro +:= "This is the "package-name" v"(version)" "dist-type" package "dir" for " + platform"." \\ + +main-exe +:= "These are some of the important "executable"s that are included:" \\ + \\ + (cond (or dr plt full) => + " "drscheme*" -- the PLT Scheme development environment" \\ \\) + " "mzscheme*" -- a text-only Scheme interpreter" \\ + (cond (or md dr plt full) => + " "mred*" -- a graphical Scheme interpreter" \\) + " "mzc*" -- command-line tool for creating executables, etc." \\ + (cond (or dr plt full) => + " "plt-help*" --- for Help (also built into DrScheme)" \\) + " "setup-plt*" --- command-line setup tool" \\ + " "planet*" --- a command-line helper for for managing third-party " + "libraries" \\ + \\ + (cond full => "This package contains the complete build tree, which " + "includes `cgc' binaries that use a conservative collector." \\ + \\) + +main-src +:= "You must compile MzScheme " (cond (or mr dr plt full) => "and MrEd ") + "before using the "package-name" software" + (cond (or dr plt full) => " (including DrScheme)")"." \\ + \\ + "For compilation instructions, see \"" + (cond win => "plt\\src\\worksp\\README" + else => "plt/src/README") + "\"." \\ +main +:= (cond src => main-src else => main-exe) + +license +:= "License" \\ + "-------" \\ \\ + "PLT Software" \\ + "Copyright (c) 1995-2003 PLT" \\ + "Copyright (c) 2004-2008 PLT Inc." \\ + \\ + "PLT software is distributed under the GNU Lesser General Public " + "License (LGPL). This means you can link PLT software (such as " + "MzScheme or MrEd) into proprietary applications, provided you follow " + "the specific rules stated in the LGPL. You can also modify PLT " + "software; if you distribute a modified version, you must distribute it " + "under the terms of the LGPL, which in particular means that you must " + "release the source code for the modified software. See " + "doc/release-notes/COPYING.LIB for more information." \\ + (cond full => + \\ "Note that this is the "package-name" distribution, which might " + "contain parts that are GPL." \\) + +more-information +:= "More Information" \\ + "----------------" \\ + \\ + "For further information, use DrScheme's `Help' menu, or run "plt-help*". " + "Also, visit http://www.plt-scheme.org/." \\ + +readme +:= intro \\ main \\ \\ license \\ \\ more-information diff --git a/collects/meta/build/sitemap/AUTHORS b/collects/meta/build/sitemap/AUTHORS new file mode 100644 index 0000000000..4858b377c7 --- /dev/null +++ b/collects/meta/build/sitemap/AUTHORS @@ -0,0 +1 @@ +opensource@google.com diff --git a/collects/meta/build/sitemap/COPYING b/collects/meta/build/sitemap/COPYING new file mode 100644 index 0000000000..e26c5fff1e --- /dev/null +++ b/collects/meta/build/sitemap/COPYING @@ -0,0 +1,37 @@ +Copyright (c) 2004, 2005, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Google Inc. nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +The sitemap_gen.py script is written in Python 2.2 and released to the open +source community for continuous improvements under the BSD 2.0 new license, +which can be found at: + + http://www.opensource.org/licenses/bsd-license.php diff --git a/collects/meta/build/sitemap/ChangeLog b/collects/meta/build/sitemap/ChangeLog new file mode 100644 index 0000000000..8fd659d2ef --- /dev/null +++ b/collects/meta/build/sitemap/ChangeLog @@ -0,0 +1,65 @@ +Wed Jun 01 01:00:00 2005 Google Inc. + + * sitemap_gen: initial release: + This directory contains Python utilities for creating + Sitemaps. + +Mon Jun 13 01:00:00 2005 Google Inc. + + * sitemap_gen.py: v1.1 + + [BIG] + Not blow up when dealing with international character encodings. + + [MODERATE] + Fix platform and Python version issues. In some versions of 2.2 + and certain platforms, True was not defined. Gak! + +Tue Jul 12 01:00:00 2005 Google Inc. + + * sitemap_gen.py: v1.2 + + [MODERATE] + Default_file option added to directory walking + Support for Extended Logfile Format (IIS's log format) + Allow wildcards in the "path" attribute on accesslog and urllist + input methods. + Running on Python 1.5 should exit cleanly with an error message + Stricter processing of configuration files + + [SMALL] + XML files written in "text" mode, so linefeeds are correct + One more Unicode issue fixed: Sitemap filenames with non-ascii + characters had still been problematic + In directory walking, the root URL of the walk now gets included + In directory walking, URLs to directories now have a "/" appended + URLs to files we recognize as our own script's Sitemap output files + are suppressed. + 'suppress_search_engine_notify="0"' now does what you would expect + Default priority on URLs is now 0.5 instead of 1.0 + Priority values written by default to only 4 decimal places + URLs to Sitemap files in the Sitemap index file are now encoded + according to the user's default_encoding, instead of forcing to UTF-8 + +Mon Aug 01 01:00:00 2005 Google Inc. + + * sitemap_gen.py: v1.3 + + [BIG] + input method added. + + [MODERATE] + Use proper IDNA encoding on international domain names. This is + only available on Python2.3 or higher. + + [SMALL] + Fixed Windows bug where directory walking would generate bad URLs on + 2+ deep subdirectories + +Wed Nov 03 01:00:00 2005 Google Inc. + + * sitemap_gen.py: v1.4 + + [SMALL] + Fixed bug where writing a gzipped sitemap would store the server's + file path in the archive. diff --git a/collects/meta/build/sitemap/PKG-INFO b/collects/meta/build/sitemap/PKG-INFO new file mode 100644 index 0000000000..dfa4c8a4f6 --- /dev/null +++ b/collects/meta/build/sitemap/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: sitemap_gen +Version: 1.4 +Summary: Sitemap Generator +Home-page: http://sourceforge.net/projects/goog-sitemapgen/ +Author: Google Inc. +Author-email: opensource@google.com +License: BSD +Description: UNKNOWN +Platform: UNKNOWN diff --git a/collects/meta/build/sitemap/README b/collects/meta/build/sitemap/README new file mode 100644 index 0000000000..e8abdbb150 --- /dev/null +++ b/collects/meta/build/sitemap/README @@ -0,0 +1,25 @@ +sitemap_gen.py + +Version 1.4 + +The sitemap_gen.py script analyzes your web server and generates one or more +Sitemap files. These files are XML listings of content you make available on +your web server. The files can be directly submitted to search engines as +hints for the search engine web crawlers as they index your web site. This +can result in better coverage of your web content in search engine indices, +and less of your bandwidth spent doing it. + +The sitemap_gen.py script is written in Python and released to the open +source community for continuous improvements under the BSD 2.0 new license, +which can be found at: + + http://www.opensource.org/licenses/bsd-license.php + +The original release notes for the script, including a walk-through for +webmasters on how to use it, can be found at the following site: + + http://www.google.com/webmasters/sitemaps/sitemap-generator.html + +The minimum Python version required is Python 2.2. However, if URLs on +your site involve any non-ASCII characters, we strongly recommend +Python 2.3 or later, as it better handles encoding issues. diff --git a/collects/meta/build/sitemap/example_config.xml b/collects/meta/build/sitemap/example_config.xml new file mode 100644 index 0000000000..2e37eaa58b --- /dev/null +++ b/collects/meta/build/sitemap/example_config.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/collects/meta/build/sitemap/example_urllist.txt b/collects/meta/build/sitemap/example_urllist.txt new file mode 100644 index 0000000000..f8192f68d9 --- /dev/null +++ b/collects/meta/build/sitemap/example_urllist.txt @@ -0,0 +1,21 @@ +# To add a list of URLs, make a space-delimited text file. The first +# column contains the URL; then you can specify various optional +# attributes in the form key=value: +# +# lastmod = modification time in ISO8601 (YYYY-MM-DDThh:mm:ss+00:00) +# changefreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | +# 'yearly' | 'never' +# priority = priority of the page relative to other pages on the same site; +# a number between 0.0 and 1.0, where 0.0 is the lowest priority +# and 1.0 is the highest priority +# +# Note that all URLs must be part of the site, and therefore must begin with +# the base_url (e.g., 'http://www.example.com/') as specified in config.xml. +# +# Any line beginning with a # is a comment. +# +# Example contents of the file: +# +# http://www.example.com/foo/bar +# http://www.example.com/foo/xxx.pdf lastmod=2003-12-31T14:05:06+00:00 +# http://www.example.com/foo/yyy?x=12&y=23 changefreq=weekly priority=0.3 diff --git a/collects/meta/build/sitemap/plt-pre.xml b/collects/meta/build/sitemap/plt-pre.xml new file mode 100644 index 0000000000..9305fc2174 --- /dev/null +++ b/collects/meta/build/sitemap/plt-pre.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/collects/meta/build/sitemap/setup.py b/collects/meta/build/sitemap/setup.py new file mode 100755 index 0000000000..fa703595d3 --- /dev/null +++ b/collects/meta/build/sitemap/setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup(name='sitemap_gen', + version='1.4', + description='Sitemap Generator', + license='BSD', + author='Google Inc.', + author_email='opensource@google.com', + url='http://sourceforge.net/projects/goog-sitemapgen/', + ) diff --git a/collects/meta/build/sitemap/sitemap_gen.py b/collects/meta/build/sitemap/sitemap_gen.py new file mode 100755 index 0000000000..cbcfd6f593 --- /dev/null +++ b/collects/meta/build/sitemap/sitemap_gen.py @@ -0,0 +1,2205 @@ +#!/usr/bin/env python +# +# Copyright (c) 2004, 2005 Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# * Neither the name of Google nor the names of its contributors may +# be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# +# The sitemap_gen.py script is written in Python 2.2 and released to +# the open source community for continuous improvements under the BSD +# 2.0 new license, which can be found at: +# +# http://www.opensource.org/licenses/bsd-license.php +# + +__usage__ = \ +"""A simple script to automatically produce sitemaps for a webserver, +in the Google Sitemap Protocol (GSP). + +Usage: python sitemap_gen.py --config=config.xml [--help] [--testing] + --config=config.xml, specifies config file location + --help, displays usage message + --testing, specified when user is experimenting +""" + +# Please be careful that all syntax used in this file can be parsed on +# Python 1.5 -- this version check is not evaluated until after the +# entire file has been parsed. +import sys +if sys.hexversion < 0x02020000: + print 'This script requires Python 2.2 or later.' + print 'Currently run with version: %s' % sys.version + sys.exit(1) + +import fnmatch +import glob +import gzip +import md5 +import os +import re +import stat +import time +import types +import urllib +import urlparse +import xml.sax + +# True and False were introduced in Python2.2.2 +try: + testTrue=True + del testTrue +except NameError: + True=1 + False=0 + +# Text encodings +ENC_ASCII = 'ASCII' +ENC_UTF8 = 'UTF-8' +ENC_IDNA = 'IDNA' +ENC_ASCII_LIST = ['ASCII', 'US-ASCII', 'US', 'IBM367', 'CP367', 'ISO646-US' + 'ISO_646.IRV:1991', 'ISO-IR-6', 'ANSI_X3.4-1968', + 'ANSI_X3.4-1986', 'CPASCII' ] +ENC_DEFAULT_LIST = ['ISO-8859-1', 'ISO-8859-2', 'ISO-8859-5'] + +# Maximum number of urls in each sitemap, before next Sitemap is created +MAXURLS_PER_SITEMAP = 50000 + +# Suffix on a Sitemap index file +SITEINDEX_SUFFIX = '_index.xml' + +# Regular expressions tried for extracting URLs from access logs. +ACCESSLOG_CLF_PATTERN = re.compile( + r'.+\s+"([^\s]+)\s+([^\s]+)\s+HTTP/\d+\.\d+"\s+200\s+.*' + ) + +# Match patterns for lastmod attributes +LASTMOD_PATTERNS = map(re.compile, [ + r'^\d\d\d\d$', + r'^\d\d\d\d-\d\d$', + r'^\d\d\d\d-\d\d-\d\d$', + r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\dZ$', + r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d[+-]\d\d:\d\d$', + r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z$', + r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?[+-]\d\d:\d\d$', + ]) + +# Match patterns for changefreq attributes +CHANGEFREQ_PATTERNS = [ + 'always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never' + ] + +# XML formats +SITEINDEX_HEADER = \ + '\n' \ + '\n' +SITEINDEX_FOOTER = '\n' +SITEINDEX_ENTRY = \ + ' \n' \ + ' %(loc)s\n' \ + ' %(lastmod)s\n' \ + ' \n' +SITEMAP_HEADER = \ + '\n' \ + '\n' +SITEMAP_FOOTER = '\n' +SITEURL_XML_PREFIX = ' \n' +SITEURL_XML_SUFFIX = ' \n' + +# Search engines to notify with the updated sitemaps +# +# This list is very non-obvious in what's going on. Here's the gist: +# Each item in the list is a 6-tuple of items. The first 5 are "almost" +# the same as the input arguments to urlparse.urlunsplit(): +# 0 - schema +# 1 - netloc +# 2 - path +# 3 - query <-- EXCEPTION: specify a query map rather than a string +# 4 - fragment +# Additionally, add item 5: +# 5 - query attribute that should be set to the new Sitemap URL +# Clear as mud, I know. +NOTIFICATION_SITES = [ + ('http', 'www.google.com', 'webmasters/sitemaps/ping', {}, '', 'sitemap') + ] + + +class Error(Exception): + """ + Base exception class. In this module we tend not to use our own exception + types for very much, but they come in very handy on XML parsing with SAX. + """ + pass +#end class Error + + +class SchemaError(Error): + """Failure to process an XML file according to the schema we know.""" + pass +#end class SchemeError + + +class Encoder: + """ + Manages wide-character/narrow-character conversions for just about all + text that flows into or out of the script. + + You should always use this class for string coercion, as opposed to + letting Python handle coercions automatically. Reason: Python + usually assumes ASCII (7-bit) as a default narrow character encoding, + which is not the kind of data we generally deal with. + + General high-level methodologies used in sitemap_gen: + + [PATHS] + File system paths may be wide or narrow, depending on platform. + This works fine, just be aware of it and be very careful to not + mix them. That is, if you have to pass several file path arguments + into a library call, make sure they are all narrow or all wide. + This class has MaybeNarrowPath() which should be called on every + file system path you deal with. + + [URLS] + URL locations are stored in Narrow form, already escaped. This has the + benefit of keeping escaping and encoding as close as possible to the format + we read them in. The downside is we may end up with URLs that have + intermingled encodings -- the root path may be encoded in one way + while the filename is encoded in another. This is obviously wrong, but + it should hopefully be an issue hit by very few users. The workaround + from the user level (assuming they notice) is to specify a default_encoding + parameter in their config file. + + [OTHER] + Other text, such as attributes of the URL class, configuration options, + etc, are generally stored in Unicode for simplicity. + """ + + def __init__(self): + self._user = None # User-specified default encoding + self._learned = [] # Learned default encodings + self._widefiles = False # File system can be wide + + # Can the file system be Unicode? + try: + self._widefiles = os.path.supports_unicode_filenames + except AttributeError: + try: + self._widefiles = sys.getwindowsversion() == os.VER_PLATFORM_WIN32_NT + except AttributeError: + pass + + # Try to guess a working default + try: + encoding = sys.getfilesystemencoding() + if encoding and not (encoding.upper() in ENC_ASCII_LIST): + self._learned = [ encoding ] + except AttributeError: + pass + + if not self._learned: + encoding = sys.getdefaultencoding() + if encoding and not (encoding.upper() in ENC_ASCII_LIST): + self._learned = [ encoding ] + + # If we had no guesses, start with some European defaults + if not self._learned: + self._learned = ENC_DEFAULT_LIST + #end def __init__ + + def SetUserEncoding(self, encoding): + self._user = encoding + #end def SetUserEncoding + + def NarrowText(self, text, encoding): + """ Narrow a piece of arbitrary text """ + if type(text) != types.UnicodeType: + return text + + # Try the passed in preference + if encoding: + try: + result = text.encode(encoding) + if not encoding in self._learned: + self._learned.append(encoding) + return result + except UnicodeError: + pass + except LookupError: + output.Warn('Unknown encoding: %s' % encoding) + + # Try the user preference + if self._user: + try: + return text.encode(self._user) + except UnicodeError: + pass + except LookupError: + temp = self._user + self._user = None + output.Warn('Unknown default_encoding: %s' % temp) + + # Look through learned defaults, knock any failing ones out of the list + while self._learned: + try: + return text.encode(self._learned[0]) + except: + del self._learned[0] + + # When all other defaults are exhausted, use UTF-8 + try: + return text.encode(ENC_UTF8) + except UnicodeError: + pass + + # Something is seriously wrong if we get to here + return text.encode(ENC_ASCII, 'ignore') + #end def NarrowText + + def MaybeNarrowPath(self, text): + """ Paths may be allowed to stay wide """ + if self._widefiles: + return text + return self.NarrowText(text, None) + #end def MaybeNarrowPath + + def WidenText(self, text, encoding): + """ Widen a piece of arbitrary text """ + if type(text) != types.StringType: + return text + + # Try the passed in preference + if encoding: + try: + result = unicode(text, encoding) + if not encoding in self._learned: + self._learned.append(encoding) + return result + except UnicodeError: + pass + except LookupError: + output.Warn('Unknown encoding: %s' % encoding) + + # Try the user preference + if self._user: + try: + return unicode(text, self._user) + except UnicodeError: + pass + except LookupError: + temp = self._user + self._user = None + output.Warn('Unknown default_encoding: %s' % temp) + + # Look through learned defaults, knock any failing ones out of the list + while self._learned: + try: + return unicode(text, self._learned[0]) + except: + del self._learned[0] + + # When all other defaults are exhausted, use UTF-8 + try: + return unicode(text, ENC_UTF8) + except UnicodeError: + pass + + # Getting here means it wasn't UTF-8 and we had no working default. + # We really don't have anything "right" we can do anymore. + output.Warn('Unrecognized encoding in text: %s' % text) + if not self._user: + output.Warn('You may need to set a default_encoding in your ' + 'configuration file.') + return text.decode(ENC_ASCII, 'ignore') + #end def WidenText +#end class Encoder +encoder = Encoder() + + +class Output: + """ + Exposes logging functionality, and tracks how many errors + we have thus output. + + Logging levels should be used as thus: + Fatal -- extremely sparingly + Error -- config errors, entire blocks of user 'intention' lost + Warn -- individual URLs lost + Log(,0) -- Un-suppressable text that's not an error + Log(,1) -- touched files, major actions + Log(,2) -- parsing notes, filtered or duplicated URLs + Log(,3) -- each accepted URL + """ + + def __init__(self): + self.num_errors = 0 # Count of errors + self.num_warns = 0 # Count of warnings + + self._errors_shown = {} # Shown errors + self._warns_shown = {} # Shown warnings + self._verbose = 0 # Level of verbosity + #end def __init__ + + def Log(self, text, level): + """ Output a blurb of diagnostic text, if the verbose level allows it """ + if text: + text = encoder.NarrowText(text, None) + if self._verbose >= level: + print text + #end def Log + + def Warn(self, text): + """ Output and count a warning. Suppress duplicate warnings. """ + if text: + text = encoder.NarrowText(text, None) + hash = md5.new(text).digest() + if not self._warns_shown.has_key(hash): + self._warns_shown[hash] = 1 + print '[WARNING] ' + text + else: + self.Log('(suppressed) [WARNING] ' + text, 3) + self.num_warns = self.num_warns + 1 + #end def Warn + + def Error(self, text): + """ Output and count an error. Suppress duplicate errors. """ + if text: + text = encoder.NarrowText(text, None) + hash = md5.new(text).digest() + if not self._errors_shown.has_key(hash): + self._errors_shown[hash] = 1 + print '[ERROR] ' + text + else: + self.Log('(suppressed) [ERROR] ' + text, 3) + self.num_errors = self.num_errors + 1 + #end def Error + + def Fatal(self, text): + """ Output an error and terminate the program. """ + if text: + text = encoder.NarrowText(text, None) + print '[FATAL] ' + text + else: + print 'Fatal error.' + sys.exit(1) + #end def Fatal + + def SetVerbose(self, level): + """ Sets the verbose level. """ + try: + if type(level) != types.IntType: + level = int(level) + if (level >= 0) and (level <= 3): + self._verbose = level + return + except ValueError: + pass + self.Error('Verbose level (%s) must be between 0 and 3 inclusive.' % level) + #end def SetVerbose +#end class Output +output = Output() + + +class URL(object): + """ URL is a smart structure grouping together the properties we + care about for a single web reference. """ + __slots__ = 'loc', 'lastmod', 'changefreq', 'priority' + + def __init__(self): + self.loc = None # URL -- in Narrow characters + self.lastmod = None # ISO8601 timestamp of last modify + self.changefreq = None # Text term for update frequency + self.priority = None # Float between 0 and 1 (inc) + #end def __init__ + + def __cmp__(self, other): + if self.loc < other.loc: + return -1 + if self.loc > other.loc: + return 1 + return 0 + #end def __cmp__ + + def TrySetAttribute(self, attribute, value): + """ Attempt to set the attribute to the value, with a pretty try + block around it. """ + if attribute == 'loc': + self.loc = self.Canonicalize(value) + else: + try: + setattr(self, attribute, value) + except AttributeError: + output.Warn('Unknown URL attribute: %s' % attribute) + #end def TrySetAttribute + + def IsAbsolute(loc): + """ Decide if the URL is absolute or not """ + if not loc: + return False + narrow = encoder.NarrowText(loc, None) + (scheme, netloc, path, query, frag) = urlparse.urlsplit(narrow) + if (not scheme) or (not netloc): + return False + return True + #end def IsAbsolute + IsAbsolute = staticmethod(IsAbsolute) + + def Canonicalize(loc): + """ Do encoding and canonicalization on a URL string """ + if not loc: + return loc + + # Let the encoder try to narrow it + narrow = encoder.NarrowText(loc, None) + + # Escape components individually + (scheme, netloc, path, query, frag) = urlparse.urlsplit(narrow) + unr = '-._~' + sub = '!$&\'()*+,;=' + netloc = urllib.quote(netloc, unr + sub + '%:@/[]') + path = urllib.quote(path, unr + sub + '%:@/') + query = urllib.quote(query, unr + sub + '%:@/?') + frag = urllib.quote(frag, unr + sub + '%:@/?') + + # Try built-in IDNA encoding on the netloc + try: + (ignore, widenetloc, ignore, ignore, ignore) = urlparse.urlsplit(loc) + for c in widenetloc: + if c >= unichr(128): + netloc = widenetloc.encode(ENC_IDNA) + netloc = urllib.quote(netloc, unr + sub + '%:@/[]') + break + except UnicodeError: + # urlsplit must have failed, based on implementation differences in the + # library. There is not much we can do here, except ignore it. + pass + except LookupError: + output.Warn('An International Domain Name (IDN) is being used, but this ' + 'version of Python does not have support for IDNA encoding. ' + ' (IDNA support was introduced in Python 2.3) The encoding ' + 'we have used instead is wrong and will probably not yield ' + 'valid URLs.') + bad_netloc = False + if '%' in netloc: + bad_netloc = True + + # Put it all back together + narrow = urlparse.urlunsplit((scheme, netloc, path, query, frag)) + + # I let '%' through. Fix any that aren't pre-existing escapes. + HEXDIG = '0123456789abcdefABCDEF' + list = narrow.split('%') + narrow = list[0] + del list[0] + for item in list: + if (len(item) >= 2) and (item[0] in HEXDIG) and (item[1] in HEXDIG): + narrow = narrow + '%' + item + else: + narrow = narrow + '%25' + item + + # Issue a warning if this is a bad URL + if bad_netloc: + output.Warn('Invalid characters in the host or domain portion of a URL: ' + + narrow) + + return narrow + #end def Canonicalize + Canonicalize = staticmethod(Canonicalize) + + def Validate(self, base_url, allow_fragment): + """ Verify the data in this URL is well-formed, and override if not. """ + assert type(base_url) == types.StringType + + # Test (and normalize) the ref + if not self.loc: + output.Warn('Empty URL') + return False + if allow_fragment: + self.loc = urlparse.urljoin(base_url, self.loc) + if not self.loc.startswith(base_url): + output.Warn('Discarded URL for not starting with the base_url: %s' % + self.loc) + self.loc = None + return False + + # Test the lastmod + if self.lastmod: + match = False + self.lastmod = self.lastmod.upper() + for pattern in LASTMOD_PATTERNS: + match = pattern.match(self.lastmod) + if match: + break + if not match: + output.Warn('Lastmod "%s" does not appear to be in ISO8601 format on ' + 'URL: %s' % (self.lastmod, self.loc)) + self.lastmod = None + + # Test the changefreq + if self.changefreq: + match = False + self.changefreq = self.changefreq.lower() + for pattern in CHANGEFREQ_PATTERNS: + if self.changefreq == pattern: + match = True + break + if not match: + output.Warn('Changefreq "%s" is not a valid change frequency on URL ' + ': %s' % (self.changefreq, self.loc)) + self.changefreq = None + + # Test the priority + if self.priority: + priority = -1.0 + try: + priority = float(self.priority) + except ValueError: + pass + if (priority < 0.0) or (priority > 1.0): + output.Warn('Priority "%s" is not a number between 0 and 1 inclusive ' + 'on URL: %s' % (self.priority, self.loc)) + self.priority = None + + return True + #end def Validate + + def MakeHash(self): + """ Provides a uniform way of hashing URLs """ + if not self.loc: + return None + if self.loc.endswith('/'): + return md5.new(self.loc[:-1]).digest() + return md5.new(self.loc).digest() + #end def MakeHash + + def Log(self, prefix='URL', level=3): + """ Dump the contents, empty or not, to the log. """ + out = prefix + ':' + + for attribute in self.__slots__: + value = getattr(self, attribute) + if not value: + value = '' + out = out + (' %s=[%s]' % (attribute, value)) + + output.Log('%s' % encoder.NarrowText(out, None), level) + #end def Log + + def WriteXML(self, file): + """ Dump non-empty contents to the output file, in XML format. """ + if not self.loc: + return + out = SITEURL_XML_PREFIX + + for attribute in self.__slots__: + value = getattr(self, attribute) + if value: + if type(value) == types.UnicodeType: + value = encoder.NarrowText(value, None) + elif type(value) != types.StringType: + value = str(value) + value = xml.sax.saxutils.escape(value) + out = out + (' <%s>%s\n' % (attribute, value, attribute)) + + out = out + SITEURL_XML_SUFFIX + file.write(out) + #end def WriteXML +#end class URL + + +class Filter: + """ + A filter on the stream of URLs we find. A filter is, in essence, + a wildcard applied to the stream. You can think of this as an + operator that returns a tri-state when given a URL: + + True -- this URL is to be included in the sitemap + None -- this URL is undecided + False -- this URL is to be dropped from the sitemap + """ + + def __init__(self, attributes): + self._wildcard = None # Pattern for wildcard match + self._regexp = None # Pattern for regexp match + self._pass = False # "Drop" filter vs. "Pass" filter + + if not ValidateAttributes('FILTER', attributes, + ('pattern', 'type', 'action')): + return + + # Check error count on the way in + num_errors = output.num_errors + + # Fetch the attributes + pattern = attributes.get('pattern') + type = attributes.get('type', 'wildcard') + action = attributes.get('action', 'drop') + if type: + type = type.lower() + if action: + action = action.lower() + + # Verify the attributes + if not pattern: + output.Error('On a filter you must specify a "pattern" to match') + elif (not type) or ((type != 'wildcard') and (type != 'regexp')): + output.Error('On a filter you must specify either \'type="wildcard"\' ' + 'or \'type="regexp"\'') + elif (action != 'pass') and (action != 'drop'): + output.Error('If you specify a filter action, it must be either ' + '\'action="pass"\' or \'action="drop"\'') + + # Set the rule + if action == 'drop': + self._pass = False + elif action == 'pass': + self._pass = True + + if type == 'wildcard': + self._wildcard = pattern + elif type == 'regexp': + try: + self._regexp = re.compile(pattern) + except re.error: + output.Error('Bad regular expression: %s' % pattern) + + # Log the final results iff we didn't add any errors + if num_errors == output.num_errors: + output.Log('Filter: %s any URL that matches %s "%s"' % + (action, type, pattern), 2) + #end def __init__ + + def Apply(self, url): + """ Process the URL, as above. """ + if (not url) or (not url.loc): + return None + + if self._wildcard: + if fnmatch.fnmatchcase(url.loc, self._wildcard): + return self._pass + return None + + if self._regexp: + if self._regexp.search(url.loc): + return self._pass + return None + + assert False # unreachable + #end def Apply +#end class Filter + + +class InputURL: + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles a single URL, manually specified in the config file. + """ + + def __init__(self, attributes): + self._url = None # The lonely URL + + if not ValidateAttributes('URL', attributes, + ('href', 'lastmod', 'changefreq', 'priority')): + return + + url = URL() + for attr in attributes.keys(): + if attr == 'href': + url.TrySetAttribute('loc', attributes[attr]) + else: + url.TrySetAttribute(attr, attributes[attr]) + + if not url.loc: + output.Error('Url entries must have an href attribute.') + return + + self._url = url + output.Log('Input: From URL "%s"' % self._url.loc, 2) + #end def __init__ + + def ProduceURLs(self, consumer): + """ Produces URLs from our data source, hands them in to the consumer. """ + if self._url: + consumer(self._url, True) + #end def ProduceURLs +#end class InputURL + + +class InputURLList: + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles a text file with a list of URLs + """ + + def __init__(self, attributes): + self._path = None # The file path + self._encoding = None # Encoding of that file + + if not ValidateAttributes('URLLIST', attributes, ('path', 'encoding')): + return + + self._path = attributes.get('path') + self._encoding = attributes.get('encoding', ENC_UTF8) + if self._path: + self._path = encoder.MaybeNarrowPath(self._path) + if os.path.isfile(self._path): + output.Log('Input: From URLLIST "%s"' % self._path, 2) + else: + output.Error('Can not locate file: %s' % self._path) + self._path = None + else: + output.Error('Urllist entries must have a "path" attribute.') + #end def __init__ + + def ProduceURLs(self, consumer): + """ Produces URLs from our data source, hands them in to the consumer. """ + + # Open the file + (frame, file) = OpenFileForRead(self._path, 'URLLIST') + if not file: + return + + # Iterate lines + linenum = 0 + for line in file.readlines(): + linenum = linenum + 1 + + # Strip comments and empty lines + if self._encoding: + line = encoder.WidenText(line, self._encoding) + line = line.strip() + if (not line) or line[0] == '#': + continue + + # Split the line on space + url = URL() + cols = line.split(' ') + for i in range(0,len(cols)): + cols[i] = cols[i].strip() + url.TrySetAttribute('loc', cols[0]) + + # Extract attributes from the other columns + for i in range(1,len(cols)): + if cols[i]: + try: + (attr_name, attr_val) = cols[i].split('=', 1) + url.TrySetAttribute(attr_name, attr_val) + except ValueError: + output.Warn('Line %d: Unable to parse attribute: %s' % + (linenum, cols[i])) + + # Pass it on + consumer(url, False) + + file.close() + if frame: + frame.close() + #end def ProduceURLs +#end class InputURLList + + +class InputDirectory: + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles a directory that acts as base for walking the filesystem. + """ + + def __init__(self, attributes, base_url): + self._path = None # The directory + self._url = None # The URL equivelant + self._default_file = None + + if not ValidateAttributes('DIRECTORY', attributes, ('path', 'url', + 'default_file')): + return + + # Prep the path -- it MUST end in a sep + path = attributes.get('path') + if not path: + output.Error('Directory entries must have both "path" and "url" ' + 'attributes') + return + path = encoder.MaybeNarrowPath(path) + if not path.endswith(os.sep): + path = path + os.sep + if not os.path.isdir(path): + output.Error('Can not locate directory: %s' % path) + return + + # Prep the URL -- it MUST end in a sep + url = attributes.get('url') + if not url: + output.Error('Directory entries must have both "path" and "url" ' + 'attributes') + return + url = URL.Canonicalize(url) + if not url.endswith('/'): + url = url + '/' + if not url.startswith(base_url): + url = urlparse.urljoin(base_url, url) + if not url.startswith(base_url): + output.Error('The directory URL "%s" is not relative to the ' + 'base_url: %s' % (url, base_url)) + return + + # Prep the default file -- it MUST be just a filename + file = attributes.get('default_file') + if file: + file = encoder.MaybeNarrowPath(file) + if os.sep in file: + output.Error('The default_file "%s" can not include path information.' + % file) + file = None + + self._path = path + self._url = url + self._default_file = file + if file: + output.Log('Input: From DIRECTORY "%s" (%s) with default file "%s"' + % (path, url, file), 2) + else: + output.Log('Input: From DIRECTORY "%s" (%s) with no default file' + % (path, url), 2) + #end def __init__ + + def ProduceURLs(self, consumer): + """ Produces URLs from our data source, hands them in to the consumer. """ + if not self._path: + return + + root_path = self._path + root_URL = self._url + root_file = self._default_file + + def PerFile(dirpath, name): + """ + Called once per file. + Note that 'name' will occasionally be None -- for a directory itself + """ + # Pull a timestamp + url = URL() + isdir = False + try: + if name: + path = os.path.join(dirpath, name) + else: + path = dirpath + isdir = os.path.isdir(path) + time = None + if isdir and root_file: + file = os.path.join(path, root_file) + try: + time = os.stat(file)[stat.ST_MTIME]; + except OSError: + pass + if not time: + time = os.stat(path)[stat.ST_MTIME]; + url.lastmod = TimestampISO8601(time) + except OSError: + pass + except ValueError: + pass + + # Build a URL + middle = dirpath[len(root_path):] + if os.sep != '/': + middle = middle.replace(os.sep, '/') + if middle: + middle = middle + '/' + if name: + middle = middle + name + if isdir: + middle = middle + '/' + url.TrySetAttribute('loc', root_URL + encoder.WidenText(middle, None)) + + # Suppress default files. (All the way down here so we can log it.) + if name and (root_file == name): + url.Log(prefix='IGNORED (default file)', level=2) + return + + consumer(url, False) + #end def PerFile + + def PerDirectory(ignore, dirpath, namelist): + """ + Called once per directory with a list of all the contained files/dirs. + """ + ignore = ignore # Avoid warnings of an unused parameter + + if not dirpath.startswith(root_path): + output.Warn('Unable to decide what the root path is for directory: ' + '%s' % dirpath) + return + + for name in namelist: + PerFile(dirpath, name) + #end def PerDirectory + + output.Log('Walking DIRECTORY "%s"' % self._path, 1) + PerFile(self._path, None) + os.path.walk(self._path, PerDirectory, None) + #end def ProduceURLs +#end class InputDirectory + + +class InputAccessLog: + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles access logs. It's non-trivial in that we want to + auto-detect log files in the Common Logfile Format (as used by Apache, + for instance) and the Extended Log File Format (as used by IIS, for + instance). + """ + + def __init__(self, attributes): + self._path = None # The file path + self._encoding = None # Encoding of that file + self._is_elf = False # Extended Log File Format? + self._is_clf = False # Common Logfile Format? + self._elf_status = -1 # ELF field: '200' + self._elf_method = -1 # ELF field: 'HEAD' + self._elf_uri = -1 # ELF field: '/foo?bar=1' + self._elf_urifrag1 = -1 # ELF field: '/foo' + self._elf_urifrag2 = -1 # ELF field: 'bar=1' + + if not ValidateAttributes('ACCESSLOG', attributes, ('path', 'encoding')): + return + + self._path = attributes.get('path') + self._encoding = attributes.get('encoding', ENC_UTF8) + if self._path: + self._path = encoder.MaybeNarrowPath(self._path) + if os.path.isfile(self._path): + output.Log('Input: From ACCESSLOG "%s"' % self._path, 2) + else: + output.Error('Can not locate file: %s' % self._path) + self._path = None + else: + output.Error('Accesslog entries must have a "path" attribute.') + #end def __init__ + + def RecognizeELFLine(self, line): + """ Recognize the Fields directive that heads an ELF file """ + if not line.startswith('#Fields:'): + return False + fields = line.split(' ') + del fields[0] + for i in range(0, len(fields)): + field = fields[i].strip() + if field == 'sc-status': + self._elf_status = i + elif field == 'cs-method': + self._elf_method = i + elif field == 'cs-uri': + self._elf_uri = i + elif field == 'cs-uri-stem': + self._elf_urifrag1 = i + elif field == 'cs-uri-query': + self._elf_urifrag2 = i + output.Log('Recognized an Extended Log File Format file.', 2) + return True + #end def RecognizeELFLine + + def GetELFLine(self, line): + """ Fetch the requested URL from an ELF line """ + fields = line.split(' ') + count = len(fields) + + # Verify status was Ok + if self._elf_status >= 0: + if self._elf_status >= count: + return None + if not fields[self._elf_status].strip() == '200': + return None + + # Verify method was HEAD or GET + if self._elf_method >= 0: + if self._elf_method >= count: + return None + if not fields[self._elf_method].strip() in ('HEAD', 'GET'): + return None + + # Pull the full URL if we can + if self._elf_uri >= 0: + if self._elf_uri >= count: + return None + url = fields[self._elf_uri].strip() + if url != '-': + return url + + # Put together a fragmentary URL + if self._elf_urifrag1 >= 0: + if self._elf_urifrag1 >= count or self._elf_urifrag2 >= count: + return None + urlfrag1 = fields[self._elf_urifrag1].strip() + urlfrag2 = None + if self._elf_urifrag2 >= 0: + urlfrag2 = fields[self._elf_urifrag2] + if urlfrag1 and (urlfrag1 != '-'): + if urlfrag2 and (urlfrag2 != '-'): + urlfrag1 = urlfrag1 + '?' + urlfrag2 + return urlfrag1 + + return None + #end def GetELFLine + + def RecognizeCLFLine(self, line): + """ Try to tokenize a logfile line according to CLF pattern and see if + it works. """ + match = ACCESSLOG_CLF_PATTERN.match(line) + recognize = match and (match.group(1) in ('HEAD', 'GET')) + if recognize: + output.Log('Recognized a Common Logfile Format file.', 2) + return recognize + #end def RecognizeCLFLine + + def GetCLFLine(self, line): + """ Fetch the requested URL from a CLF line """ + match = ACCESSLOG_CLF_PATTERN.match(line) + if match: + request = match.group(1) + if request in ('HEAD', 'GET'): + return match.group(2) + return None + #end def GetCLFLine + + def ProduceURLs(self, consumer): + """ Produces URLs from our data source, hands them in to the consumer. """ + + # Open the file + (frame, file) = OpenFileForRead(self._path, 'ACCESSLOG') + if not file: + return + + # Iterate lines + for line in file.readlines(): + if self._encoding: + line = encoder.WidenText(line, self._encoding) + line = line.strip() + + # If we don't know the format yet, try them both + if (not self._is_clf) and (not self._is_elf): + self._is_elf = self.RecognizeELFLine(line) + self._is_clf = self.RecognizeCLFLine(line) + + # Digest the line + match = None + if self._is_elf: + match = self.GetELFLine(line) + elif self._is_clf: + match = self.GetCLFLine(line) + if not match: + continue + + # Pass it on + url = URL() + url.TrySetAttribute('loc', match) + consumer(url, True) + + file.close() + if frame: + frame.close() + #end def ProduceURLs +#end class InputAccessLog + + +class InputSitemap(xml.sax.handler.ContentHandler): + + """ + Each Input class knows how to yield a set of URLs from a data source. + + This one handles Sitemap files and Sitemap index files. For the sake + of simplicity in design (and simplicity in interfacing with the SAX + package), we do not handle these at the same time, recursively. Instead + we read an index file completely and make a list of Sitemap files, then + go back and process each Sitemap. + """ + + class _ContextBase(object): + + """Base class for context handlers in our SAX processing. A context + handler is a class that is responsible for understanding one level of + depth in the XML schema. The class knows what sub-tags are allowed, + and doing any processing specific for the tag we're in. + + This base class is the API filled in by specific context handlers, + all defined below. + """ + + def __init__(self, subtags): + """Initialize with a sequence of the sub-tags that would be valid in + this context.""" + self._allowed_tags = subtags # Sequence of sub-tags we can have + self._last_tag = None # Most recent seen sub-tag + #end def __init__ + + def AcceptTag(self, tag): + """Returns True iff opening a sub-tag is valid in this context.""" + valid = tag in self._allowed_tags + if valid: + self._last_tag = tag + else: + self._last_tag = None + return valid + #end def AcceptTag + + def AcceptText(self, text): + """Returns True iff a blurb of text is valid in this context.""" + return False + #end def AcceptText + + def Open(self): + """The context is opening. Do initialization.""" + pass + #end def Open + + def Close(self): + """The context is closing. Return our result, if any.""" + pass + #end def Close + + def Return(self, result): + """We're returning to this context after handling a sub-tag. This + method is called with the result data from the sub-tag that just + closed. Here in _ContextBase, if we ever see a result it means + the derived child class forgot to override this method.""" + if result: + raise NotImplementedError + #end def Return + #end class _ContextBase + + class _ContextUrlSet(_ContextBase): + + """Context handler for the document node in a Sitemap.""" + + def __init__(self): + InputSitemap._ContextBase.__init__(self, ('url',)) + #end def __init__ + #end class _ContextUrlSet + + class _ContextUrl(_ContextBase): + + """Context handler for a URL node in a Sitemap.""" + + def __init__(self, consumer): + """Initialize this context handler with the callable consumer that + wants our URLs.""" + InputSitemap._ContextBase.__init__(self, URL.__slots__) + self._url = None # The URL object we're building + self._consumer = consumer # Who wants to consume it + #end def __init__ + + def Open(self): + """Initialize the URL.""" + assert not self._url + self._url = URL() + #end def Open + + def Close(self): + """Pass the URL to the consumer and reset it to None.""" + assert self._url + self._consumer(self._url, False) + self._url = None + #end def Close + + def Return(self, result): + """A value context has closed, absorb the data it gave us.""" + assert self._url + if result: + self._url.TrySetAttribute(self._last_tag, result) + #end def Return + #end class _ContextUrl + + class _ContextSitemapIndex(_ContextBase): + + """Context handler for the document node in an index file.""" + + def __init__(self): + InputSitemap._ContextBase.__init__(self, ('sitemap',)) + self._loclist = [] # List of accumulated Sitemap URLs + #end def __init__ + + def Open(self): + """Just a quick verify of state.""" + assert not self._loclist + #end def Open + + def Close(self): + """Return our list of accumulated URLs.""" + if self._loclist: + temp = self._loclist + self._loclist = [] + return temp + #end def Close + + def Return(self, result): + """Getting a new loc URL, add it to the collection.""" + if result: + self._loclist.append(result) + #end def Return + #end class _ContextSitemapIndex + + class _ContextSitemap(_ContextBase): + + """Context handler for a Sitemap entry in an index file.""" + + def __init__(self): + InputSitemap._ContextBase.__init__(self, ('loc', 'lastmod')) + self._loc = None # The URL to the Sitemap + #end def __init__ + + def Open(self): + """Just a quick verify of state.""" + assert not self._loc + #end def Open + + def Close(self): + """Return our URL to our parent.""" + if self._loc: + temp = self._loc + self._loc = None + return temp + output.Warn('In the Sitemap index file, a "sitemap" entry had no "loc".') + #end def Close + + def Return(self, result): + """A value has closed. If it was a 'loc', absorb it.""" + if result and (self._last_tag == 'loc'): + self._loc = result + #end def Return + #end class _ContextSitemap + + class _ContextValue(_ContextBase): + + """Context handler for a single value. We return just the value. The + higher level context has to remember what tag led into us.""" + + def __init__(self): + InputSitemap._ContextBase.__init__(self, ()) + self._text = None + #end def __init__ + + def AcceptText(self, text): + """Allow all text, adding it to our buffer.""" + if self._text: + self._text = self._text + text + else: + self._text = text + return True + #end def AcceptText + + def Open(self): + """Initialize our buffer.""" + self._text = None + #end def Open + + def Close(self): + """Return what's in our buffer.""" + text = self._text + self._text = None + if text: + text = text.strip() + return text + #end def Close + #end class _ContextValue + + def __init__(self, attributes): + """Initialize with a dictionary of attributes from our entry in the + config file.""" + xml.sax.handler.ContentHandler.__init__(self) + self._pathlist = None # A list of files + self._current = -1 # Current context in _contexts + self._contexts = None # The stack of contexts we allow + self._contexts_idx = None # ...contexts for index files + self._contexts_stm = None # ...contexts for Sitemap files + + if not ValidateAttributes('SITEMAP', attributes, ['path']): + return + + # Init the first file path + path = attributes.get('path') + if path: + path = encoder.MaybeNarrowPath(path) + if os.path.isfile(path): + output.Log('Input: From SITEMAP "%s"' % path, 2) + self._pathlist = [path] + else: + output.Error('Can not locate file "%s"' % path) + else: + output.Error('Sitemap entries must have a "path" attribute.') + #end def __init__ + + def ProduceURLs(self, consumer): + """In general: Produces URLs from our data source, hand them to the + callable consumer. + + In specific: Iterate over our list of paths and delegate the actual + processing to helper methods. This is a complexity no other data source + needs to suffer. We are unique in that we can have files that tell us + to bring in other files. + + Note the decision to allow an index file or not is made in this method. + If we call our parser with (self._contexts == None) the parser will + grab whichever context stack can handle the file. IE: index is allowed. + If instead we set (self._contexts = ...) before parsing, the parser + will only use the stack we specify. IE: index not allowed. + """ + # Set up two stacks of contexts + self._contexts_idx = [InputSitemap._ContextSitemapIndex(), + InputSitemap._ContextSitemap(), + InputSitemap._ContextValue()] + + self._contexts_stm = [InputSitemap._ContextUrlSet(), + InputSitemap._ContextUrl(consumer), + InputSitemap._ContextValue()] + + # Process the first file + assert self._pathlist + path = self._pathlist[0] + self._contexts = None # We allow an index file here + self._ProcessFile(path) + + # Iterate over remaining files + self._contexts = self._contexts_stm # No index files allowed + for path in self._pathlist[1:]: + self._ProcessFile(path) + #end def ProduceURLs + + def _ProcessFile(self, path): + """Do per-file reading/parsing/consuming for the file path passed in.""" + assert path + + # Open our file + (frame, file) = OpenFileForRead(path, 'SITEMAP') + if not file: + return + + # Rev up the SAX engine + try: + self._current = -1 + xml.sax.parse(file, self) + except SchemaError: + output.Error('An error in file "%s" made us abort reading the Sitemap.' + % path) + except IOError: + output.Error('Cannot read from file "%s"' % path) + except xml.sax._exceptions.SAXParseException, e: + output.Error('XML error in the file "%s" (line %d, column %d): %s' % + (path, e._linenum, e._colnum, e.getMessage())) + + # Clean up + file.close() + if frame: + frame.close() + #end def _ProcessFile + + def _MungeLocationListIntoFiles(self, urllist): + """Given a list of URLs, munge them into our self._pathlist property. + We do this by assuming all the files live in the same directory as + the first file in the existing pathlist. That is, we assume a + Sitemap index points to Sitemaps only in the same directory. This + is not true in general, but will be true for any output produced + by this script. + """ + assert self._pathlist + path = self._pathlist[0] + path = os.path.normpath(path) + dir = os.path.dirname(path) + wide = False + if type(path) == types.UnicodeType: + wide = True + + for url in urllist: + url = URL.Canonicalize(url) + output.Log('Index points to Sitemap file at: %s' % url, 2) + (scheme, netloc, path, query, frag) = urlparse.urlsplit(url) + file = os.path.basename(path) + file = urllib.unquote(file) + if wide: + file = encoder.WidenText(file) + if dir: + file = dir + os.sep + file + if file: + self._pathlist.append(file) + output.Log('Will attempt to read Sitemap file: %s' % file, 1) + #end def _MungeLocationListIntoFiles + + def startElement(self, tag, attributes): + """SAX processing, called per node in the config stream. + As long as the new tag is legal in our current context, this + becomes an Open call on one context deeper. + """ + # If this is the document node, we may have to look for a context stack + if (self._current < 0) and not self._contexts: + assert self._contexts_idx and self._contexts_stm + if tag == 'urlset': + self._contexts = self._contexts_stm + elif tag == 'sitemapindex': + self._contexts = self._contexts_idx + output.Log('File is a Sitemap index.', 2) + else: + output.Error('The document appears to be neither a Sitemap nor a ' + 'Sitemap index.') + raise SchemaError + + # Display a kinder error on a common mistake + if (self._current < 0) and (self._contexts == self._contexts_stm) and ( + tag == 'sitemapindex'): + output.Error('A Sitemap index can not refer to another Sitemap index.') + raise SchemaError + + # Verify no unexpected attributes + if attributes: + text = '' + for attr in attributes.keys(): + # The document node will probably have namespaces + if self._current < 0: + if attr.find('xmlns') >= 0: + continue + if attr.find('xsi') >= 0: + continue + if text: + text = text + ', ' + text = text + attr + if text: + output.Warn('Did not expect any attributes on any tag, instead tag ' + '"%s" had attributes: %s' % (tag, text)) + + # Switch contexts + if (self._current < 0) or (self._contexts[self._current].AcceptTag(tag)): + self._current = self._current + 1 + assert self._current < len(self._contexts) + self._contexts[self._current].Open() + else: + output.Error('Can not accept tag "%s" where it appears.' % tag) + raise SchemaError + #end def startElement + + def endElement(self, tag): + """SAX processing, called per node in the config stream. + This becomes a call to Close on one context followed by a call + to Return on the previous. + """ + tag = tag # Avoid warning on unused argument + assert self._current >= 0 + retval = self._contexts[self._current].Close() + self._current = self._current - 1 + if self._current >= 0: + self._contexts[self._current].Return(retval) + elif retval and (self._contexts == self._contexts_idx): + self._MungeLocationListIntoFiles(retval) + #end def endElement + + def characters(self, text): + """SAX processing, called when text values are read. Important to + note that one single text value may be split across multiple calls + of this method. + """ + if (self._current < 0) or ( + not self._contexts[self._current].AcceptText(text)): + if text.strip(): + output.Error('Can not accept text "%s" where it appears.' % text) + raise SchemaError + #end def characters +#end class InputSitemap + + +class FilePathGenerator: + """ + This class generates filenames in a series, upon request. + You can request any iteration number at any time, you don't + have to go in order. + + Example of iterations for '/path/foo.xml.gz': + 0 --> /path/foo.xml.gz + 1 --> /path/foo1.xml.gz + 2 --> /path/foo2.xml.gz + _index.xml --> /path/foo_index.xml + """ + + def __init__(self): + self.is_gzip = False # Is this a GZIP file? + + self._path = None # '/path/' + self._prefix = None # 'foo' + self._suffix = None # '.xml.gz' + #end def __init__ + + def Preload(self, path): + """ Splits up a path into forms ready for recombination. """ + path = encoder.MaybeNarrowPath(path) + + # Get down to a base name + path = os.path.normpath(path) + base = os.path.basename(path).lower() + if not base: + output.Error('Couldn\'t parse the file path: %s' % path) + return False + lenbase = len(base) + + # Recognize extension + lensuffix = 0 + compare_suffix = ['.xml', '.xml.gz', '.gz'] + for suffix in compare_suffix: + if base.endswith(suffix): + lensuffix = len(suffix) + break + if not lensuffix: + output.Error('The path "%s" doesn\'t end in a supported file ' + 'extension.' % path) + return False + self.is_gzip = suffix.endswith('.gz') + + # Split the original path + lenpath = len(path) + self._path = path[:lenpath-lenbase] + self._prefix = path[lenpath-lenbase:lenpath-lensuffix] + self._suffix = path[lenpath-lensuffix:] + + return True + #end def Preload + + def GeneratePath(self, instance): + """ Generates the iterations, as described above. """ + prefix = self._path + self._prefix + if type(instance) == types.IntType: + if instance: + return '%s%d%s' % (prefix, instance, self._suffix) + return prefix + self._suffix + return prefix + instance + #end def GeneratePath + + def GenerateURL(self, instance, root_url): + """ Generates iterations, but as a URL instead of a path. """ + prefix = root_url + self._prefix + retval = None + if type(instance) == types.IntType: + if instance: + retval = '%s%d%s' % (prefix, instance, self._suffix) + else: + retval = prefix + self._suffix + else: + retval = prefix + instance + return URL.Canonicalize(retval) + #end def GenerateURL + + def GenerateWildURL(self, root_url): + """ Generates a wildcard that should match all our iterations """ + prefix = URL.Canonicalize(root_url + self._prefix) + temp = URL.Canonicalize(prefix + self._suffix) + suffix = temp[len(prefix):] + return prefix + '*' + suffix + #end def GenerateURL +#end class FilePathGenerator + + +class PerURLStatistics: + """ Keep track of some simple per-URL statistics, like file extension. """ + + def __init__(self): + self._extensions = {} # Count of extension instances + #end def __init__ + + def Consume(self, url): + """ Log some stats for the URL. At the moment, that means extension. """ + if url and url.loc: + (scheme, netloc, path, query, frag) = urlparse.urlsplit(url.loc) + if not path: + return + + # Recognize directories + if path.endswith('/'): + if self._extensions.has_key('/'): + self._extensions['/'] = self._extensions['/'] + 1 + else: + self._extensions['/'] = 1 + return + + # Strip to a filename + i = path.rfind('/') + if i >= 0: + assert i < len(path) + path = path[i:] + + # Find extension + i = path.rfind('.') + if i > 0: + assert i < len(path) + ext = path[i:].lower() + if self._extensions.has_key(ext): + self._extensions[ext] = self._extensions[ext] + 1 + else: + self._extensions[ext] = 1 + else: + if self._extensions.has_key('(no extension)'): + self._extensions['(no extension)'] = self._extensions[ + '(no extension)'] + 1 + else: + self._extensions['(no extension)'] = 1 + #end def Consume + + def Log(self): + """ Dump out stats to the output. """ + if len(self._extensions): + output.Log('Count of file extensions on URLs:', 1) + set = self._extensions.keys() + set.sort() + for ext in set: + output.Log(' %7d %s' % (self._extensions[ext], ext), 1) + #end def Log + +class Sitemap(xml.sax.handler.ContentHandler): + """ + This is the big workhorse class that processes your inputs and spits + out sitemap files. It is built as a SAX handler for set up purposes. + That is, it processes an XML stream to bring itself up. + """ + + def __init__(self, suppress_notify): + xml.sax.handler.ContentHandler.__init__(self) + self._filters = [] # Filter objects + self._inputs = [] # Input objects + self._urls = {} # Maps URLs to count of dups + self._set = [] # Current set of URLs + self._filegen = None # Path generator for output files + self._wildurl1 = None # Sitemap URLs to filter out + self._wildurl2 = None # Sitemap URLs to filter out + self._sitemaps = 0 # Number of output files + # We init _dup_max to 2 so the default priority is 0.5 instead of 1.0 + self._dup_max = 2 # Max number of duplicate URLs + self._stat = PerURLStatistics() # Some simple stats + self._in_site = False # SAX: are we in a Site node? + self._in_Site_ever = False # SAX: were we ever in a Site? + + self._default_enc = None # Best encoding to try on URLs + self._base_url = None # Prefix to all valid URLs + self._store_into = None # Output filepath + self._suppress = suppress_notify # Suppress notify of servers + #end def __init__ + + def ValidateBasicConfig(self): + """ Verifies (and cleans up) the basic user-configurable options. """ + all_good = True + + if self._default_enc: + encoder.SetUserEncoding(self._default_enc) + + # Canonicalize the base_url + if all_good and not self._base_url: + output.Error('A site needs a "base_url" attribute.') + all_good = False + if all_good and not URL.IsAbsolute(self._base_url): + output.Error('The "base_url" must be absolute, not relative: %s' % + self._base_url) + all_good = False + if all_good: + self._base_url = URL.Canonicalize(self._base_url) + if not self._base_url.endswith('/'): + self._base_url = self._base_url + '/' + output.Log('BaseURL is set to: %s' % self._base_url, 2) + + # Load store_into into a generator + if all_good: + if self._store_into: + self._filegen = FilePathGenerator() + if not self._filegen.Preload(self._store_into): + all_good = False + else: + output.Error('A site needs a "store_into" attribute.') + all_good = False + + # Ask the generator for patterns on what its output will look like + if all_good: + self._wildurl1 = self._filegen.GenerateWildURL(self._base_url) + self._wildurl2 = self._filegen.GenerateURL(SITEINDEX_SUFFIX, + self._base_url) + + # Unify various forms of False + if all_good: + if self._suppress: + if (type(self._suppress) == types.StringType) or (type(self._suppress) + == types.UnicodeType): + if (self._suppress == '0') or (self._suppress.lower() == 'false'): + self._suppress = False + + # Done + if not all_good: + output.Log('See "example_config.xml" for more information.', 0) + return all_good + #end def ValidateBasicConfig + + def Generate(self): + """ Run over all the Inputs and ask them to Produce """ + # Run the inputs + for input in self._inputs: + input.ProduceURLs(self.ConsumeURL) + + # Do last flushes + if len(self._set): + self.FlushSet() + if not self._sitemaps: + output.Warn('No URLs were recorded, writing an empty sitemap.') + self.FlushSet() + + # Write an index as needed + if self._sitemaps > 1: + self.WriteIndex() + + # Notify + self.NotifySearch() + + # Dump stats + self._stat.Log() + #end def Generate + + def ConsumeURL(self, url, allow_fragment): + """ + All per-URL processing comes together here, regardless of Input. + Here we run filters, remove duplicates, spill to disk as needed, etc. + """ + if not url: + return + + # Validate + if not url.Validate(self._base_url, allow_fragment): + return + + # Run filters + accept = None + for filter in self._filters: + accept = filter.Apply(url) + if accept != None: + break + if not (accept or (accept == None)): + url.Log(prefix='FILTERED', level=2) + return + + # Ignore our out output URLs + if fnmatch.fnmatchcase(url.loc, self._wildurl1) or fnmatch.fnmatchcase( + url.loc, self._wildurl2): + url.Log(prefix='IGNORED (output file)', level=2) + return + + # Note the sighting + hash = url.MakeHash() + if self._urls.has_key(hash): + dup = self._urls[hash] + if dup > 0: + dup = dup + 1 + self._urls[hash] = dup + if self._dup_max < dup: + self._dup_max = dup + url.Log(prefix='DUPLICATE') + return + + # Acceptance -- add to set + self._urls[hash] = 1 + self._set.append(url) + self._stat.Consume(url) + url.Log() + + # Flush the set if needed + if len(self._set) >= MAXURLS_PER_SITEMAP: + self.FlushSet() + #end def ConsumeURL + + def FlushSet(self): + """ + Flush the current set of URLs to the output. This is a little + slow because we like to sort them all and normalize the priorities + before dumping. + """ + + # Sort and normalize + output.Log('Sorting and normalizing collected URLs.', 1) + self._set.sort() + for url in self._set: + hash = url.MakeHash() + dup = self._urls[hash] + if dup > 0: + self._urls[hash] = -1 + if not url.priority: + url.priority = '%.4f' % (float(dup) / float(self._dup_max)) + + # Get the filename we're going to write to + filename = self._filegen.GeneratePath(self._sitemaps) + if not filename: + output.Fatal('Unexpected: Couldn\'t generate output filename.') + self._sitemaps = self._sitemaps + 1 + output.Log('Writing Sitemap file "%s" with %d URLs' % + (filename, len(self._set)), 1) + + # Write to it + frame = None + file = None + + try: + if self._filegen.is_gzip: + basename = os.path.basename(filename); + frame = open(filename, 'wb') + file = gzip.GzipFile(fileobj=frame, filename=basename, mode='wt') + else: + file = open(filename, 'wt') + + file.write(SITEMAP_HEADER) + for url in self._set: + url.WriteXML(file) + file.write(SITEMAP_FOOTER) + + file.close() + if frame: + frame.close() + + frame = None + file = None + except IOError: + output.Fatal('Couldn\'t write out to file: %s' % filename) + os.chmod(filename, 0644) + + # Flush + self._set = [] + #end def FlushSet + + def WriteIndex(self): + """ Write the master index of all Sitemap files """ + # Make a filename + filename = self._filegen.GeneratePath(SITEINDEX_SUFFIX) + if not filename: + output.Fatal('Unexpected: Couldn\'t generate output index filename.') + output.Log('Writing index file "%s" with %d Sitemaps' % + (filename, self._sitemaps), 1) + + # Make a lastmod time + lastmod = TimestampISO8601(time.time()) + + # Write to it + try: + fd = open(filename, 'wt') + fd.write(SITEINDEX_HEADER) + + for mapnumber in range(0,self._sitemaps): + # Write the entry + mapurl = self._filegen.GenerateURL(mapnumber, self._base_url) + mapattributes = { 'loc' : mapurl, 'lastmod' : lastmod } + fd.write(SITEINDEX_ENTRY % mapattributes) + + fd.write(SITEINDEX_FOOTER) + + fd.close() + fd = None + except IOError: + output.Fatal('Couldn\'t write out to file: %s' % filename) + os.chmod(filename, 0644) + #end def WriteIndex + + def NotifySearch(self): + """ Send notification of the new Sitemap(s) to the search engines. """ + if self._suppress: + output.Log('Search engine notification is suppressed.', 1) + return + + output.Log('Notifying search engines.', 1) + + # Override the urllib's opener class with one that doesn't ignore 404s + class ExceptionURLopener(urllib.FancyURLopener): + def http_error_default(self, url, fp, errcode, errmsg, headers): + output.Log('HTTP error %d: %s' % (errcode, errmsg), 2) + raise IOError + #end def http_error_default + #end class ExceptionURLOpener + old_opener = urllib._urlopener + urllib._urlopener = ExceptionURLopener() + + # Build the URL we want to send in + if self._sitemaps > 1: + url = self._filegen.GenerateURL(SITEINDEX_SUFFIX, self._base_url) + else: + url = self._filegen.GenerateURL(0, self._base_url) + + # Test if we can hit it ourselves + try: + u = urllib.urlopen(url) + u.close() + except IOError: + output.Error('When attempting to access our generated Sitemap at the ' + 'following URL:\n %s\n we failed to read it. Please ' + 'verify the store_into path you specified in\n' + ' your configuration file is web-accessable. Consult ' + 'the FAQ for more\n information.' % url) + output.Warn('Proceeding to notify with an unverifyable URL.') + + # Cycle through notifications + # To understand this, see the comment near the NOTIFICATION_SITES comment + for ping in NOTIFICATION_SITES: + query_map = ping[3] + query_attr = ping[5] + query_map[query_attr] = url + query = urllib.urlencode(query_map) + notify = urlparse.urlunsplit((ping[0], ping[1], ping[2], query, ping[4])) + + # Send the notification + output.Log('Notifying: %s' % ping[1], 1) + output.Log('Notification URL: %s' % notify, 2) + try: + u = urllib.urlopen(notify) + u.read() + u.close() + except IOError: + output.Warn('Cannot contact: %s' % ping[1]) + + if old_opener: + urllib._urlopener = old_opener + #end def NotifySearch + + def startElement(self, tag, attributes): + """ SAX processing, called per node in the config stream. """ + + if tag == 'site': + if self._in_site: + output.Error('Can not nest Site entries in the configuration.') + else: + self._in_site = True + + if not ValidateAttributes('SITE', attributes, + ('verbose', 'default_encoding', 'base_url', 'store_into', + 'suppress_search_engine_notify')): + return + + verbose = attributes.get('verbose', 0) + if verbose: + output.SetVerbose(verbose) + + self._default_enc = attributes.get('default_encoding') + self._base_url = attributes.get('base_url') + self._store_into = attributes.get('store_into') + if not self._suppress: + self._suppress = attributes.get('suppress_search_engine_notify', + False) + self.ValidateBasicConfig() + + elif tag == 'filter': + self._filters.append(Filter(attributes)) + + elif tag == 'url': + self._inputs.append(InputURL(attributes)) + + elif tag == 'urllist': + for attributeset in ExpandPathAttribute(attributes, 'path'): + self._inputs.append(InputURLList(attributeset)) + + elif tag == 'directory': + self._inputs.append(InputDirectory(attributes, self._base_url)) + + elif tag == 'accesslog': + for attributeset in ExpandPathAttribute(attributes, 'path'): + self._inputs.append(InputAccessLog(attributeset)) + + elif tag == 'sitemap': + for attributeset in ExpandPathAttribute(attributes, 'path'): + self._inputs.append(InputSitemap(attributeset)) + + else: + output.Error('Unrecognized tag in the configuration: %s' % tag) + #end def startElement + + def endElement(self, tag): + """ SAX processing, called per node in the config stream. """ + if tag == 'site': + assert self._in_site + self._in_site = False + self._in_site_ever = True + #end def endElement + + def endDocument(self): + """ End of SAX, verify we can proceed. """ + if not self._in_site_ever: + output.Error('The configuration must specify a "site" element.') + else: + if not self._inputs: + output.Warn('There were no inputs to generate a sitemap from.') + #end def endDocument +#end class Sitemap + + +def ValidateAttributes(tag, attributes, goodattributes): + """ Makes sure 'attributes' does not contain any attribute not + listed in 'goodattributes' """ + all_good = True + for attr in attributes.keys(): + if not attr in goodattributes: + output.Error('Unknown %s attribute: %s' % (tag, attr)) + all_good = False + return all_good +#end def ValidateAttributes + +def ExpandPathAttribute(src, attrib): + """ Given a dictionary of attributes, return a list of dictionaries + with all the same attributes except for the one named attrib. + That one, we treat as a file path and expand into all its possible + variations. """ + # Do the path expansion. On any error, just return the source dictionary. + path = src.get(attrib) + if not path: + return [src] + path = encoder.MaybeNarrowPath(path); + pathlist = glob.glob(path) + if not pathlist: + return [src] + + # If this isn't actually a dictionary, make it one + if type(src) != types.DictionaryType: + tmp = {} + for key in src.keys(): + tmp[key] = src[key] + src = tmp + + # Create N new dictionaries + retval = [] + for path in pathlist: + dst = src.copy() + dst[attrib] = path + retval.append(dst) + + return retval +#end def ExpandPathAttribute + +def OpenFileForRead(path, logtext): + """ Opens a text file, be it GZip or plain """ + + frame = None + file = None + + if not path: + return (frame, file) + + try: + if path.endswith('.gz'): + frame = open(path, 'rb') + file = gzip.GzipFile(fileobj=frame, mode='rt') + else: + file = open(path, 'rt') + + if logtext: + output.Log('Opened %s file: %s' % (logtext, path), 1) + else: + output.Log('Opened file: %s' % path, 1) + except IOError: + output.Error('Can not open file: %s' % path) + + return (frame, file) +#end def OpenFileForRead + +def TimestampISO8601(t): + """Seconds since epoch (1970-01-01) --> ISO 8601 time string.""" + return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(t)) +#end def TimestampISO8601 + +def CreateSitemapFromFile(configpath, suppress_notify): + """ Sets up a new Sitemap object from the specified configuration file. """ + + # Remember error count on the way in + num_errors = output.num_errors + + # Rev up SAX to parse the config + sitemap = Sitemap(suppress_notify) + try: + output.Log('Reading configuration file: %s' % configpath, 0) + xml.sax.parse(configpath, sitemap) + except IOError: + output.Error('Cannot read configuration file: %s' % configpath) + except xml.sax._exceptions.SAXParseException, e: + output.Error('XML error in the config file (line %d, column %d): %s' % + (e._linenum, e._colnum, e.getMessage())) + except xml.sax._exceptions.SAXReaderNotAvailable: + output.Error('Some installs of Python 2.2 did not include complete support' + ' for XML.\n Please try upgrading your version of Python' + ' and re-running the script.') + + # If we added any errors, return no sitemap + if num_errors == output.num_errors: + return sitemap + return None +#end def CreateSitemapFromFile + +def ProcessCommandFlags(args): + """ + Parse command line flags per specified usage, pick off key, value pairs + All flags of type "--key=value" will be processed as __flags[key] = value, + "--option" will be processed as __flags[option] = option + """ + + flags = {} + rkeyval = '--(?P\S*)[=](?P\S*)' # --key=val + roption = '--(?P