#! /usr/bin/env python ID = "$Id: release,v 1.50 2016/01/25 04:24:21 eagle Exp $" # # release -- Release a software package. # # Copyright 2001, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2013, 2014 # Russ Allbery # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. import ConfigParser import calendar import getopt import os import re import shutil import stat import string import subprocess import sys import time # The path to the configuration file specifying how to release software. CONFIG = os.path.expanduser("~/dvl/CONFIG/release.conf") def die(message): sys.stderr.write("release: %s\n" % message) sys.exit(1) def usage(): """Print usage information for this program. """ return "Usage: release [-bdshuv] \n" def get_version(): """Return the version number of this program. Returns the verison number of this program as a string containing the program name, the CVS revision number, and the last modification date in parentheses. This is taken from the global ID variable. """ # Make sure CVS doesn't see a variable here to substitute. if ID != "$" + "Id$": data = ID.split() date = data[3].replace("/", "-") program = data[1][:-2] version = data[2] return program + " " + version + " (" + date + ")" else: return "" def version_deb(path): """Find the upstream version from a Debian changelog file. Given a path to a directory that contains a debian subdirectory and a changelog file in that subdirectory, use dpkg-parsechangelog to determine the version of the latest revision in that changelog and from that the upstream version of the package. """ old = os.getcwd() os.chdir(path) info = os.popen("dpkg-parsechangelog", "r") version_regex = re.compile(r"^Version: (?:\d+:)?(.*?)-\d") line = info.readline() while line: match = version_regex.search(line) if match: info.close() os.chdir(old) return match.group(1) line = info.readline() die("cannot find version information in " + path) def version_script(filename): """Find the version and last modified date of a script. Extracts the version and last modified date of a script given by the filename argument and returns them as a tuple. The last modified date is taken from the RCS Id string and the version is taken either from that string or from a setting of the variable VERSION if one is found. The setting of VERSION overrides the Id string. Missing information is given as empty strings. The version is returned as a string, and the date is returned in seconds since epoch. """ file = open(filename, "r") id_regex = re.compile(r"\$Id\: (.*)\$") version_regex = re.compile(r"\$?VERSION\s*=\s*'?([\d.]+)") id = version = "" date = None for i in range(50): line = file.readline() if not line: break match = id_regex.search(line) if match: id = match.group(1) match = version_regex.match(line) if match: version = match.group(1) file.close() if id: idversion, date, hour = string.split(id)[1:4] if not version: version = idversion pretty = date + " " + hour + " UTC" try: date = time.strptime(pretty, "%Y-%m-%d %H:%M:%S %Z") except ValueError: date = time.strptime(pretty, "%Y/%m/%d %H:%M:%S %Z") date = calendar.timegm(date) else: date = int(time.time()) return (version, date) def find_tarball(directory, pattern): """Find a tarball matching a pattern and return file and version. Given a directory and a pattern matching files in that directory, find the release tarball there. Returns a tuple consisting of the full path to that tarball, the version embedded in the name, and the timestamp of the tarball. The file name should be the contents of the first set of capturing parentheses in the pattern, and the version should be the contents of the second. """ file_regex = re.compile(pattern) files = os.listdir(directory) for file in files: match = file_regex.search(file) if match: path = directory + "/" + match.group(1) time = os.stat(directory + '/' + file).st_mtime return (path, match.group(2), time) return (None, None, None) def move_files(root, pattern, dest): """Find files matching pattern in root and move them to dest. """ file_regex = re.compile(pattern) files = os.listdir(root) for file in files: match = file_regex.search(file) if match: os.rename(root + "/" + file, dest + "/" + file) def rename_dir(root, pattern, dest): """Find a directory matching pattern in root and rename it to dest. """ dir_regex = re.compile(pattern) files = os.listdir(root) for file in files: match = dir_regex.search(file) if match and os.path.isdir(root + "/" + file): os.rename(root + "/" + file, dest) return die("unable to find directory matching " + pattern + " in " + root) def cvs_export_tree(module, dest, cvsroot = None): """Export the given module into a directory. Given a directory name and a CVS module, export the given CVS module into that directory. Optionally takes a cvsroot to pass to CVS. Dies on failure. """ command = "cvs" if cvsroot: command = command + " -d " + cvsroot command = command + " export -kv -r HEAD -d " + dest + " " + module status = os.system(command) if status: die("cvs export status " + `status`) def svn_export_tree(url, dest): """Export the given Subversion repository to a directory. Given a directory name and a Subversion URL, export the given Subversion repository into that directory. Dies on failure. """ command = "svn export " + url + " " + dest status = os.system(command) if status: die("svn export status " + `status`) def bzr_export_tree(url, dest): """Export the given bzr repository to a directory. Given a directory name and a bzr URL, export the given bzr repository to that directory. Dies on failure. """ command = "bzr branch " + url + " " + dest status = os.system(command) if status: die("bzr branch status " + `status`) def git_export_tree(url, dest, branch = "master"): """Export the given git repository to a directory. Given a directory name and a URL or path to a Git repository, export the repository to that directory. Dies on failure. """ command = "git archive --remote=" + url + " --prefix=" + dest + "/" command = command + " " + branch + " | tar xf -" status = os.system(command) if status: die("git archive status " + `status`) def cvs_checkout_debian_tree(module, dest, cvsroot = None): """Check out the debian directory from CVS over an existing tree. Given a directory name and a CVS module, check out the Debian subdirectory of the given CVS module into that directory, removing any existing debian subdirectory. Optionally takes a cvsroot to pass to CVS. Dies on failure. """ debian = dest + "/debian" module = module + "/debian" try: shutil.rmtree(debian) except OSError: pass command = "cvs" if cvsroot: command = command + " -d " + cvsroot command = command + " export -r HEAD -kv -d " + debian + " " + module status = os.system(command) if status: die("cvs export status " + `status`) def svn_checkout_debian_tree(url, dest): """Check out the debian directory from Subversion over an existing tree. Given a directory name and a Subversion URL, check out the debian subdirectory at the given URL into that directory, removing any existing debian subdirectory. Dies on failure. """ debian = dest + "/debian" url = url + "/debian" try: shutil.rmtree(debian) except OSError: pass command = "svn export " + url + " " + debian status = os.system(command) if status: die("svn export status " + `status`) def cvs_export_file(file, dest, cvsroot = None): """Export the given file from CVS. Given a file path in CVS and the location to put an export of that file, do the equivalent of an export of that file (permanently expanding all CVS keywords) and put it into the destination path. If cvsroot isn't given, use the CVS default. """ command = "cvs -Q" if cvsroot: command = command + " -d " + cvsroot command = command + " checkout -kv -p " + file output = open(dest, "w") input = os.popen(command) for line in input: output.write(line) status = input.close() output.close() if status: die("cvs checkout status " + `status`) def handle_script(program, config): """Release a script and return the version and time. Performs the necessary release steps for a single-file release, like a script. Handles cvs export or copying the script, and returns the version and modification time taken from the script. """ path = config.get(program, "script") (version, time) = version_script(path) if config.has_option(program, "copy"): shutil.copy2(path, config.get(program, "copy")) elif config.has_option(program, "export"): try: cvsroot = config.get(program, "cvsroot") except ConfigParser.NoOptionError: cvsroot = None path = config.get(program, "cvspath") cvs_export_file(path, config.get(program, "export"), cvsroot) return (version, time) def archive_tarball(path, release, archive, pattern, version): """Copy a tarball to a release area and archive old versions. Given the path to the new tarball, the path to the release directory, the path to the archive directory, a pattern matching tarballs for the same program, and the current release, copy the tarball into the release area. If archive is not None, also scan the release directory for any files matching pattern. For each of those files, if the first match group doesn't match version (so if the release is for some other, presumably older version), move it into the archive directory, which is created if necessary. """ if not os.path.isdir(release): die("release path " + release + " is not a directory") shutil.copy2(path + '.gz', release) shutil.copy2(path + '.gz.asc', release) if os.path.isfile(path + '.xz'): shutil.copy2(path + '.xz', release) shutil.copy2(path + '.xz.asc', release) base = os.path.basename(path) link = re.sub('-' + version.replace('.', '\\.'), '', base) for ext in ('.gz', '.xz'): if os.path.islink(release + '/' + link + ext): os.unlink(release + '/' + link + ext) os.unlink(release + '/' + link + ext + '.asc') os.symlink(base + '.gz', release + '/' + link + '.gz') os.symlink(base + '.gz.asc', release + '/' + link + '.gz.asc') if os.path.isfile(path + '.xz'): os.symlink(base + '.xz', release + '/' + link + '.xz') os.symlink(base + '.xz.asc', release + '/' + link + '.xz.asc') if archive: file_regex = re.compile(pattern) files = os.listdir(release) move = [] for file in files: match = file_regex.search(file) if match and match.group(2) != version: move.append(file) if len(move) > 0: if not os.path.isdir(archive): os.mkdir(archive, 0777) for file in move: shutil.move(release + "/" + file, archive + "/" + file) return (version, time) def handle_tarball(program, config): """Release a tarball and return the version and time. Performs the necessary release steps for a tarball release. Handles copying the tarball to the archive and archiving old versions, and returns the version and modification time taken from the tarball. """ dir = config.get(program, "tarball") prefix = config.get(program, "prefix") pattern = '^(' + prefix + '-(\d[^-]*)\.tar)\.(xz|gz)' (path, version, time) = find_tarball(dir, pattern + '$') if not path: die("cannot find tarball in " + dir) for ext in ('gz', 'xz'): file = path + '.' + ext if (ext == 'xz') and not os.path.isfile(file): continue signature = file + '.asc' if os.path.isfile(signature): modtime = os.stat(file).st_mtime if os.stat(signature).st_mtime < modtime: os.remove(signature) if not os.path.isfile(signature): command = ['gpg2', '--detach-sign', '--armor'] if config.has_option(program, "keyid"): for keyid in config.get(program, "keyid").split(): command.extend(['-u', keyid]) command.append(file) subprocess.check_call(command) if config.has_option(program, "copy"): release = config.get(program, "copy") archive = None if config.has_option(program, "archive"): archive = config.get(program, "archive") archive_tarball(path, release, archive, pattern + '(\.[^.]*)?', version) return (version, time) def update_versions(versions, program, version, timestamp): """Update the .versions file for a new release. Updates the .versions file used by spin for a new release of a particular product. This is done by writing the existing .versions file to .versions.new, replacing the existing version number and release date in local time. If this is successful, .versions.new is renamed to .versions and then committed into CVS. """ new_versions = versions + ".new" old = open(versions, "r") new = open(new_versions, "w") new_date = time.strftime("%Y-%m-%d", time.localtime(timestamp)) new_time = time.strftime("%H:%M:%S", time.localtime(timestamp)) found = 0 for line in old: product, old_version, old_date, old_time = line.split()[0:4] if product == program: while len(old_version) > len(version): version += " " while len(version) > len(old_version): old_version += " " line = line.replace(old_version, version, 1) line = line.replace(old_date, new_date, 1) line = line.replace(old_time, new_time, 1) found = 1 new.write(line) if not found: sys.stdout.write("Dependency page: ") depend = string.rstrip(sys.stdin.readline()) full_date = new_date + " " + new_time line = "%-15s %-6s %-20s %s\n" \ % (program, version, full_date, depend) new.write(line) old.close() new.close() os.rename(new_versions, versions) path, file = os.path.split(versions) os.chdir(path) print "Added versions entry for " + program + " " + version command = "git commit -m '" + program + " " + version + "' " + file status = os.system(command) if status: die("git commit status " + `status`) def deb_build(program, config, builddir = "/tmp", source = False): """Build a Debian package from an export and the current release. Locates the last major release of the package, unpacks it, checks out the current debian directory over top of that unpacked tarball, and then runs pdebuild in the resulting source tree to generate new Debian packages. Those packages will be left in builddir. If source is true, append -sa to the build options. """ package = config.get(program, "package") release = config.get(program, "copy") prefix = config.get(program, "prefix") pattern = '^(' + prefix + ')-([^-]*)\.tar\.gz$' (orig, version, time) = find_tarball(release, pattern) os.chdir(builddir) shutil.copy2(orig, package + "_" + version + ".orig.tar.gz") os.system("tar xfz " + package + "_" + version + ".orig.tar.gz") pattern = prefix + "-(.*)" rename_dir('.', pattern, package + "-" + version) path = builddir + "/" + package + "-" + version if config.has_option(program, "cvspath"): module = config.get(program, "cvspath") try: cvsroot = config.get(program, "cvsroot") except ConfigParser.NoOptionError: cvsroot = None cvs_checkout_debian_tree(module, path, cvsroot) elif config.has_option(program, "svnurl"): url = config.get(program, "svnurl") svn_checkout_debian_tree(url, path) else: die("neither cvspath nor svnurl set for " + program) os.chdir(path) command = "pdebuild --buildresult .. --debbuildopts " if source: command += "'-i -sa'" else: command += "-i" status = os.system(command) if status: die("build status " + `status`) os.chdir("..") os.system("rm *_source.changes") shutil.rmtree(path) def deb_build_script(program, config, builddir = "/tmp", source = False): """Build a Debian package around a single script. Checks out the Debian packaging instructions for a script and uses the build-orig target to retrieve the script and build the .orig tarball, and then runs pdebuild in the resulting source tree to generate a new Debian package. That package will be left in builddir. If source is true, append -sa to the build options. """ package = config.get(program, "package") os.chdir(builddir) try: cvsroot = config.get(program, "cvsroot") except ConfigParser.NoOptionError: cvsroot = None module = config.get(program, "cvsdebpath") cvs_export_tree(module, package, cvsroot) version = version_deb(package) path = package + "-" + version os.rename(package, path) os.chdir(path) status = os.system("debian/rules build-orig") if status: die("build-orig status " + `status`) command = "pdebuild --buildresult .. --debbuildopts " if source: command += "'-i -sa'" else: command += "-i" status = os.system(command) if status: die("build status " + `status`) os.chdir("..") os.system("rm *_source.changes") shutil.rmtree(path) def make_dist(program, config, distdir): """Generate a distribution tarball of a package in distdir. Exports the current source of a package into an empty directory and then performs whatever actions are required to generate a distribution tarball. The resulting tarball is left in distdir. If there is no xz-compressed version of the tarball, create one. """ os.chdir(distdir) if config.has_option(program, "cvspath"): module = config.get(program, "cvspath") try: cvsroot = config.get(program, "cvsroot") except ConfigParser.NoOptionError: cvsroot = None cvs_export_tree(module, program, cvsroot) elif config.has_option(program, "svnurl"): url = config.get(program, "svnurl") svn_export_tree(url, program) elif config.has_option(program, "bzrurl"): url = config.get(program, "bzrurl") bzr_export_tree(url, program) elif config.has_option(program, "giturl"): url = config.get(program, "giturl") if config.has_option(program, "gitbranch"): branch = config.get(program, "gitbranch") git_export_tree(url, program, branch) else: git_export_tree(url, program) else: die("no source repository set for " + program) os.chdir(program) if os.path.isfile("Build.PL"): status = os.system("perl Build.PL") if status: die("perl Build.PL status " + `status`) status = os.system("./Build disttest") if status: die("./Build disttest status " + `status`) status = os.system("./Build dist") if status: die("./Build dist status " + `status`) elif os.path.isfile("Makefile.PL"): status = os.system("perl Makefile.PL") if status: die("perl Makefile.PL status " + `status`) status = os.system("make dist") if status: die("make dist status " + `status`) elif os.path.isfile("autogen") or os.path.isfile("bootstrap"): if os.path.isfile("autogen"): status = os.system("./autogen") if status: die("autogen status " + `status`) else: status = os.system("./bootstrap") if status: die("bootstrap status " + `status`) status = os.system("./configure") if status: die("configure status " + `status`) if config.has_option(program, "maketarget"): status = os.system("make " + config.get(program, "maketarget")) elif os.path.isfile("Makefile.am"): status = os.system("make distcheck") else: status = os.system("make dist") if status: die("make dist status " + `status`) elif os.path.isfile("Makefile"): if config.has_option(program, "maketarget"): status = os.system("make " + config.get(program, "maketarget")) else: status = os.system("make dist") if status: die("make dist status " + `status`) else: die("unknown package type for " + program) prefix = config.get(program, "prefix") pattern = '^(' + prefix + '-(.*)\.tar)\.(xz|gz)' (path, version, time) = find_tarball('.', pattern + '$') if not os.path.isfile(path + '.xz'): gzip_file = path + '.gz' subprocess.check_call(['gzip', '-dk', gzip_file]) subprocess.check_call(['xz', path]) move_files('.', pattern, "..") os.chdir("..") shutil.rmtree(program) def find_unreleased(config, versions): """Find and report on unreleased scripts. Walks through the .versions file and, for each program listed there for which we can easily determine the current released version, check to be sure the released version matches the version in the .versions file. Report on any cases where it doesn't. """ data = open(versions, "r") for line in data: product, version = line.split()[0:2] if config.has_section(product): try: path = config.get(product, "script") current = version_script(path)[0] if current != version: print "%s (%s != %s)" % (product, version, current) except ConfigParser.NoOptionError: continue data.close() def main(): longopts = ["build", "dist", "help", "source", "unreleased", "version"] options, arguments = getopt.getopt(sys.argv[1:], "bdhsuv", longopts) build = False dist = False source = False unreleased = False for opt, arg in options: if opt in ("-b", "--build"): build = True elif opt in ("-d", "--dist"): dist = True elif opt in ("-s", "--source"): source = True elif opt in ("-u", "--unreleased"): unreleased = True elif opt in ("-h", "--help"): print usage() sys.exit() elif opt in ("-v", "--version"): print get_version() sys.exit() if not unreleased and len(arguments) != 1: sys.stderr.write(usage()) sys.exit(1) config = ConfigParser.ConfigParser() config.readfp(open(CONFIG)) versions = config.get("PATHS", "versions") if unreleased: find_unreleased(config, versions) sys.exit(0) program = arguments[0] if not config.has_section(program): die("unknown program " + program) if build: if config.has_option("PATHS", "builddir"): builddir = config.get("PATHS", "builddir") else: builddir = None if config.has_option(program, "cvsdebpath"): deb_build_script(program, config, builddir, source) else: deb_build(program, config, builddir, source) elif dist: make_dist(program, config, config.get("PATHS", "distdir")) else: if config.has_option(program, "script"): (version, time) = handle_script(program, config) elif config.has_option(program, "tarball"): (version, time) = handle_tarball(program, config) else: die(program + " has neither a script nor tarball configuration") update_versions(versions, program, version, time) if __name__ == "__main__": main() sys.exit() documentation = """ =head1 NAME release - Release a software package =head1 SYNOPSIS B [B<-hsv>] [B<-b> | B<-d>] I B B<-u> =head1 REQUIREMENTS Python 2.5 or later, GnuPG for signing releases, and the revision control program for whatever revision control software is used for the source. Building Debian packages additionally requires pbuilder be installed and properly configured. Git is used to commit changes to the F<.verisons> file. This program is very likely to require customization if used by anyone other than its author. =head1 DESCRIPTION B automates portions of the process of releasing a new version of a software package or script. It's actions and all relevant paths are controlled by a configuration file (see L below). It understands a variety of revision control systems and build systems and attempts to determine the correct thing to do automatically with a minimum of configuration. It updates a F<.versions> file for use by B for generating web pages. The default action, if no arguments but I are given, is to create a detached GnuPG signature of the package if it is a tarball release (not a script), copy it to its copy location, create or update symlinks without the embedded version to point to the latest version, archive old versions if appropriate, and update F<.versions>. In this mode, it expects the tarball or script to have already been prepared for a release, although for a script it may export the final version from CVS. If the B<-d> option is given, B instead generates a tarball release by running the appropriate build actions. It currently understands how to do exports from CVS, Subversion, bzr, and Git and how to generate tarball releases for packages using Automake, Perl's build system, or a dist Makefile target, with or without a configure script. If the B<-b> option is given, B instead builds Debian packages. It knows how to combine a tarball release with a F directory stored in CVS or Subversion to build a package. All package builds are done with B. This mode is no longer tested. If the B<-u> option is given, B does not expect a package on the command line. Instead, it scans the F<.versions> file and checks the release version against the current version information, as best as it is able to determine, for each package listed there. For any that appear to have a newer release than is recorded in the F<.versions> file, it prints out the package and its current version. Currently, it only checks scripts. =head1 OPTIONS =over 4 =item B<-b>, B<--build> Build a Debian package as described above. This option is no longer tested and is not used by the author, who has switched to B and separate repository branches for Debian package builds. =item B<-d>, B<--dist> Rather than release software, generate the release tarball. B first exports the package from its revision control system and then runs the appropriate build system commands to generate a release tarball. It understands CVS, Subversion, bzr, and Git for revision control systems and Perl, Automake, Autoconf, and simple makefiles with a C target for generating the release tarball. =item B<-h>, B<--help> Prints basic usage information for B. =item B<-s>, B<--source> When building a Debian package with B<-b>, force inclusion of the full source in the *.changes file by passing B<-sa> to B in the B<--debuildopts> argument. =item B<-u>, B<--unreleased> Rather than release software, scan the F<.versions> file and check all scripts listed there to see if they have a different version than the one recorded in F<.versions>. =item B<-v>, B<--version> Print out the version of B and exit. =back =head1 CONFIG FILE B uses a configuration file for most of the information it needs. This configuration file is in the format parsed by the standard Python ConfigFile class (similar to a Windows INI file) and should have one section per package, as used for command-line arguments. It also must have a section entitled C that configures some global variables. Blank lines and lines beginning with C<#> are ignored. Sections start with the section title between C<[]>. A variable setting is the variable name, a colon, whitespace, and then the value. For example: [PATHS] distdir: /home/eagle/data/dist versions: /home/eagle/web/eagle/.versions [kstart] tarball: /home/eagle/data/dist prefix: kstart copy: /afs/ir/users/r/r/rra/public/software/kerberos archive: /afs/ir/users/r/r/rra/public/software/ARCHIVE/kstart package: kstart giturl: /home/eagle/dvl/kstart Variables for the C section: =over 4 =item builddir Specifies where to put Debian packages built using the B<-b> option. Required only if B<-b> is used. =item distdir Specifies where to put tarballs built using the B<-d> option. This location will then generally be named in the tarball parameter of the relevant package configuration section. Required only if B<-d> is used. =item versions Specifies the location of the F<.versions> file for B that B should update. Required. =back Variables for each package section: =over 4 =item archive When copying a new tarball release, look for any older files from previous versions and move them to this directory, which is created if it doesn't already exist. Older files are recognized by looking for files starting with C and matching the file name pattern for a tarball with a version or its MD5 checksum or PGP signature. Only used if C is set; there is no way to archive old versions of scripts. =item bzrurl The URL to the bzr repository for the package. When B<-d> is given, it is used with the C command to generate an exported copy of the repository, which is then used to run the appropriate command to generate a distribution tarball. =item copy The location into which to copy the release, either a script or a tarball. For tarballs, files from previous versions in that location will be left there unless C is also set. For scripts, any existing version will be overwritten. =item cvsdebpath The path to the CVS module for the Debian build rules within a CVS repository. This is only used when building a Debian package for a script using the B<-b> option and is no longer tested. =item cvspath The path to the CVS module for the package within a CVS repository. When B<-d> is given, it is used with the C command to generate an exported copy of the repository, which is then used to run the appropriate command to generate a distribution tarball. It is also used to locate the script in the CVS repository to generate a CVS export when the C variable is set instead of C. If C is not also set, the default CVS root is used, which may be unpredictable. =item cvsroot Specifies the root of the CVS repository for use with C. Setting this variable is strongly recommended, even if you have CVS configured to use a default repository and all packages are in that repository, since it removes potential future ambiguity. =item export Normally, scripts are copied from the location specified by C