# builder.py vi:ts=4:sw=4:expandtab:
#
# Copyright (c) 2006 Three Rings Design, 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:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. 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.
# 3. Neither the name of the copyright owner nor the names of 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.
from twisted.internet import reactor, defer, protocol, threads
from twisted.python import threadable
threadable.init()
import os, re, gzip, shutil, exceptions, glob
import cStringIO
import farb
from farb import utils
# make(1) path
MAKE_PATH = '/usr/bin/make'
# chroot(8) path
CHROOT_PATH = '/usr/sbin/chroot'
# cvs(1) path
CVS_PATH = '/usr/bin/cvs'
# mdconfig(8) path
MDCONFIG_PATH = '/sbin/mdconfig'
# mount(8) path
MOUNT_PATH = '/sbin/mount'
# umount(8) path
UMOUNT_PATH = '/sbin/umount'
# portsnap(8) path
PORTSNAP_PATH = '/usr/sbin/portsnap'
# tar(1) path
TAR_PATH = '/usr/bin/tar'
# chflags(1) path
CHFLAGS_PATH = '/bin/chflags'
# Standard FreeBSD src location
FREEBSD_REL_PATH = '/usr/src/release'
# Standard FreeBSD ports location
FREEBSD_PORTS_PATH = '/usr/ports'
# Relative path of newvers.sh file in the FreeBSD CVS repository
NEWVERS_PATH = 'src/sys/conf/newvers.sh'
# Release root-relative path to the contents of the first install CD
RELEASE_CD_PATH = 'R/cdrom/disc1'
# Package chroot-relative path to the package directory
RELEASE_PACKAGE_PATH = 'usr/ports/packages'
# Path to root of filesystem. This is mostly here to override in unit tests
ROOT_PATH = '/'
# Releative path for /etc/resolv.conf
RESOLV_CONF = 'etc/resolv.conf'
# Default Root Environment
ROOT_ENV = {
'USER' : 'root',
'GROUP' : 'wheel',
'HOME' : '/root',
'LOGNAME' : 'root',
'PATH' : '/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:/usr/X11R6/bin:/root/bin',
'FTP_PASSIVE_MODE' : 'YES'
}
# Exceptions
class CommandError(farb.FarbError):
pass
class CVSCommandError(CommandError):
pass
class MountCommandError(CommandError):
pass
class NCVSParseError(CVSCommandError):
pass
class MDConfigCommandError(CommandError):
pass
class MakeCommandError(CommandError):
pass
class PortsnapCommandError(CommandError):
pass
class ChflagsCommandError(CommandError):
pass
class ReleaseBuildError(farb.FarbError):
pass
class ISOReaderError(farb.FarbError):
pass
class PackageChrootAssemblerError(farb.FarbError):
pass
class PackageBuildError(farb.FarbError):
pass
class InstallAssembleError(farb.FarbError):
pass
class ReleaseAssembleError(farb.FarbError):
pass
class NetInstallAssembleError(farb.FarbError):
pass
class CDReleaseError(farb.FarbError):
pass
class ChrootCleanerError(farb.FarbError):
pass
class LoggingProcessProtocol(protocol.ProcessProtocol):
"""
make(1) process protocol
"""
def __init__(self, deferred, log):
"""
@param deferred: Deferred to call with process return code
@param log: Open log file
"""
self.log = log
self.d = deferred
def outReceived(self, data):
self.log.write(data)
def errReceived(self, data):
self.log.write(data)
def connectionMade(self):
# We're not interested in writing to stdin
self.transport.closeStdin()
def processEnded(self, status):
if (status.value.exitCode == 0):
self.d.callback(status.value.exitCode)
else:
self.d.errback(CommandError(status.value.exitCode))
class NCVSBuildnameProcessProtocol(protocol.ProcessProtocol):
"""
FreeBSD CVS newvers.sh information extraction.
Extracts the release build name from a copy of newvers.sh written by
cvs(1) to stdout.
"""
_buffer = ''
delimiter = '\n'
def __init__(self, deferred):
"""
@param deferred: Deferred to call with process return code
"""
self.d = deferred
self.shVarRegex = re.compile(r'^([A-Za-z]+)="([A-Za-z0-9\.\-]+)"')
self.fbsdRevision = None
self.fbsdBranch = None
def outReceived(self, data):
"""
Searches a shell script for lines matching VARIABLE=VALUE,
looking for the FreeBSD revision and branch variable assignments
Uses line-oriented input buffering.
"""
# Split the input into lines
lines = (self._buffer + data).split(self.delimiter)
# Pop the last (potentially incomplete) line
self._buffer = lines.pop(-1)
# Search for the revision and branch variables
for line in lines:
vmatch = re.search(self.shVarRegex, line)
if (vmatch):
if (vmatch.group(1) == 'REVISION'):
self.fbsdRevision = vmatch.group(2)
elif (vmatch.group(1) == 'BRANCH'):
self.fbsdBranch = vmatch.group(2)
def processEnded(self, status):
if (status.value.exitCode != 0):
self.d.errback(CVSCommandError('cvs(1) returned %d' % status.value.exitCode))
return
if (not self.fbsdRevision or not self.fbsdBranch):
self.d.errback(NCVSParseError('Could not parse both REVISION and BRANCH variables'))
return
self.d.callback(self.fbsdRevision+ '-' + self.fbsdBranch)
class MDConfigProcessProtocol(protocol.ProcessProtocol):
"""
FreeBSD mdconfig(1) protocol.
Attach and detach vnode md(4) devices.
"""
_buffer = ''
def __init__(self, deferred):
"""
@param deferred: Deferred to call with process return code
"""
self.d = deferred
def outReceived(self, data):
"""
mdconfig(1) will output the md(4) device name
"""
self._buffer = self._buffer.join(data)
def processEnded(self, status):
if (status.value.exitCode != 0):
self.d.errback(MDConfigCommandError('mdconfig returned %d' % status.value.exitCode))
return
self.d.callback(self._buffer.rstrip('\n'))
class MDConfigCommand(object):
"""
mdconfig(8) command context
"""
def __init__(self, file):
"""
Create a new MDConfigCommand vnode instance
@param file: File to attach
"""
self.file = file
self.md = None
def _cbAttached(self, result):
self.md = result
def attach(self):
"""
Attach the file to an md(4) device
"""
assert(self.md == None)
# Create command argv
argv = [MDCONFIG_PATH, '-a', '-t', 'vnode', '-f', self.file]
d = defer.Deferred()
protocol = MDConfigProcessProtocol(d)
reactor.spawnProcess(protocol, MDCONFIG_PATH, args=argv, env=ROOT_ENV)
d.addCallback(self._cbAttached)
return d
def detach(self):
"""
Attach the file to an md(4) device
"""
assert(self.md)
# Create command argv
argv = [MDCONFIG_PATH, '-d', '-u', self.md]
d = defer.Deferred()
protocol = MDConfigProcessProtocol(d)
reactor.spawnProcess(protocol, MDCONFIG_PATH, args=argv, env=ROOT_ENV)
return d
class CVSCommand(object):
"""
cvs(1) command context
"""
def __init__(self, repository):
"""
Create a new CVSCommand instance
@param repository: CVS repository to use
"""
self.repository = repository
def _ebCVS(self, failure):
# Provide a more specific exception type
failure.trap(CommandError)
raise CVSCommandError, failure.value
def checkout(self, release, module, destination, log):
"""
Run cvs(1) checkout
@param release: release to checkout, ex. HEAD
@param module: module to use, ex: ports
@param destination: destination for the checkout
@param log: Open log file
"""
# Create command argv
d = defer.Deferred()
d.addErrback(self._ebCVS)
protocol = LoggingProcessProtocol(d, log)
argv = [CVS_PATH, '-R', '-d', self.repository, 'checkout', '-r', release, '-d', destination, module]
reactor.spawnProcess(protocol, CVS_PATH, args=argv, env=ROOT_ENV)
return d
class MountCommand(object):
"""
mount(8)/umount(8) command context
"""
# Work around mount/umount() race condition
# in FreeBSD 6.0's vfs code.
mountLock = defer.DeferredLock()
def __init__(self, device, mountpoint, fstype=None):
"""
Create a new MountCommand instance
@param device: device to mount
@param mountpoint: mount point
@param fstype: File system type. If unspecified, mount(8) will
try to figure it out.
"""
self.device = device
self.mountpoint = mountpoint
self.fstype = fstype
def _ebMount(self, failure):
# Release the lock
self.mountLock.release()
# Provide a more specific exception type
failure.trap(CommandError)
raise MountCommandError, failure.value
def _cbReleaseMountLock(self, result):
self.mountLock.release()
return result
def mount(self, log):
"""
Run mount(8)
@param log: Open log file
"""
# Create command argv
d = defer.Deferred()
d.addErrback(self._ebMount)
protocol = LoggingProcessProtocol(d, log)
if (self.fstype):
argv = [MOUNT_PATH, '-t', self.fstype, self.device, self.mountpoint]
else:
argv = [MOUNT_PATH, self.device, self.mountpoint]
lock = self.mountLock.acquire()
lock.addCallback(lambda _: reactor.spawnProcess(protocol, MOUNT_PATH, args=argv, env=ROOT_ENV))
d.addCallback(self._cbReleaseMountLock)
return d
def umount(self, log):
"""
Run umount(8)
@param log: Open log file
"""
# Create command argv
d = defer.Deferred()
d.addErrback(self._ebMount)
protocol = LoggingProcessProtocol(d, log)
argv = [UMOUNT_PATH, self.mountpoint]
lock = self.mountLock.acquire()
lock.addCallback(lambda _: reactor.spawnProcess(protocol, UMOUNT_PATH, args=argv, env=ROOT_ENV))
d.addCallback(self._cbReleaseMountLock)
return d
class MDMountCommand(MountCommand):
"""
mount(8)/umount(8) command context
"""
def __init__(self, mdc, mountpoint, fstype=None):
"""
Create a new MountCommand instance
@param mdc: MDConfigCommand instance
@param mountpoint: mount point
@param fstype: File system type. If unspecified, mount(8) will
try to figure it out.
"""
self.mdc = mdc
super(MDMountCommand, self).__init__(None, mountpoint, fstype)
def _ebUnmount(self, failure):
# Provide a more specific exception type
failure.trap(CommandError)
raise MountCommandError, failure.value
def _cbUnmount(self, result):
"""
Device unmounted, detach it
"""
d = self.mdc.detach()
d.addCallback(self._cbDetach, result)
return d
def _cbAttach(self, result, log):
"""
Device attached, mount it
"""
# build the device path
self.device = os.path.join('/dev/', self.mdc.md)
return super(MDMountCommand, self).mount(log)
def _cbDetach(self, result, mountExitCode):
# Return the mount(8) exit code
return mountExitCode
def mount(self, log):
"""
Attach the device, run mount(8)
@param log: Open log file
"""
# Attach the image. Let the caller
# handle MDConfigCommand exceptions
# directly
d = self.mdc.attach()
d.addCallback(self._cbAttach, log)
return d
def umount(self, log):
"""
Run unmount(8), detach the image.
@param log: Open log file
"""
# Unmount the image, detach it on success.
#
# Clean up the CommandError exception
# on failure.
d = super(MDMountCommand, self).umount(log)
d.addCallbacks(self._cbUnmount, self._ebUnmount)
return d
class MakeCommand(object):
"""
make(1) command context
"""
def __init__(self, directory, targets, options={}, chrootdir=None):
"""
Create a new MakeCommand instance
@param directory: Directory in which to run make(1)
@param targets: Makefile targets
@param options: Dictionary of Makefile options
@param chrootdir: Optional chroot directory
"""
self.directory = directory
self.targets = targets
self.options = options
self.chrootdir = chrootdir
def _ebMake(self, failure):
# Provide a more specific exception type
failure.trap(CommandError)
raise MakeCommandError, failure.value
def make(self, log):
"""
Run make(1)
@param log: Open log file
"""
# Create command argv
d = defer.Deferred()
d.addErrback(self._ebMake)
protocol = LoggingProcessProtocol(d, log)
argv = [MAKE_PATH, '-C', self.directory]
if self.chrootdir:
runCmd = CHROOT_PATH
argv.insert(0, self.chrootdir)
argv.insert(0, CHROOT_PATH)
else:
runCmd = MAKE_PATH
for target in self.targets:
argv.append(target)
for option, value in self.options.items():
argv.append("%s=%s" % (option, value))
reactor.spawnProcess(protocol, runCmd, args=argv, env=ROOT_ENV)
return d
class PortsnapCommand(object):
"""
portsnap(8) command context
"""
def _ebPortsnap(self, failure):
# Provide a more specific exception type
failure.trap(CommandError)
raise PortsnapCommandError, failure.value
def fetch(self, log):
"""
Run portsnap(8) fetch to get an up-to-date ports tree snapshot
@param log: Open log file
"""
d = defer.Deferred()
d.addErrback(self._ebPortsnap)
protocol = LoggingProcessProtocol(d, log)
argv = [PORTSNAP_PATH, 'fetch']
reactor.spawnProcess(protocol, PORTSNAP_PATH, args=argv, env=ROOT_ENV, usePTY=True)
return d
def extract(self, destination, log):
"""
Run portsnap(8) extract in a ports directory
@param destination: Ports directory to extract to
@param log: Open log file
"""
d = defer.Deferred()
d.addErrback(self._ebPortsnap)
protocol = LoggingProcessProtocol(d, log)
argv = [PORTSNAP_PATH, '-p', destination, 'extract']
reactor.spawnProcess(protocol, PORTSNAP_PATH, args=argv, env=ROOT_ENV)
return d
class ChflagsCommand(object):
"""
chflags(1) command context
"""
def __init__(self, path):
"""
Create a new ChflagsCommand instance
@param path: Path to file or directory whose flags will be changed
"""
self.path = path
def _ebChflags(self, failure):
failure.trap(CommandError)
raise ChflagsCommandError, failure.value
def removeAll(self, log):
"""
Recursively remove all flags from self.path
@param log: Open log file
"""
d = defer.Deferred()
d.addErrback(self._ebChflags)
protocol = LoggingProcessProtocol(d, log)
argv = [CHFLAGS_PATH, '-R', '0', self.path]
reactor.spawnProcess(protocol, CHFLAGS_PATH, args=argv, env=ROOT_ENV)
return d
class ChrootCleaner(object):
"""
Delete a chroot directory completely, then create a new empty directory in
its place
"""
def __init__(self, chroot):
"""
Create a new ChrootCleaner
@param chroot Directory that needs to be wiped out
"""
self.chroot = chroot
def _ebClean(self, failure):
try:
failure.raiseException()
except ChflagsCommandError, e:
raise ChrootCleanerError, "Error removing file flags in %s: %s" % (self.chroot, e)
except Exception, e:
raise ChrootCleanerError, "Error cleaning %s: %s" % (self.chroot, e)
def clean(self, log):
"""
Recursively remove all files from self.chroot
@param log: Open log file
"""
if (os.path.exists(self.chroot)):
# Remove all flags on files in the chroot, then delete it all
if os.path.exists(self.chroot):
cc = ChflagsCommand(self.chroot)
d = cc.removeAll(log)
d.addCallback(lambda _: threads.deferToThread(shutil.rmtree, self.chroot))
# Now create new empty directory
d.addCallback(lambda _: os.mkdir(self.chroot))
# If the chroot isn't there, all we need to do is create it
else:
d = threads.deferToThread(os.mkdir, self.chroot)
d.addErrback(self._ebClean)
return d
class ReleaseBuilder(object):
makeTarget = ('release',)
defaultMakeOptions = {
'NOPORTS' : 'no',
'NODOC' : 'no'
}
"""
Build a FreeBSD Release
"""
def __init__(self, cvsroot, cvstag, chroot, makecds=False):
"""
Create a new ReleaseBuilder instance.
@param cvsroot: Path to FreeBSD CVS Repository
@param cvstag: FreeBSD Release Tag
@param chroot: chroot build directory
@param makecds: Boolean enables the creation of ISO CD installation images.
"""
self.cvsroot = cvsroot
self.cvstag = cvstag
self.chroot = chroot
self.makecds = makecds
def _ebBuildError(self, failure):
try:
failure.raiseException()
except CVSCommandError, e:
raise ReleaseBuildError, "An error occured extracting the release name from \"%s\": %s" % (self.cvsroot, e)
except MakeCommandError, e:
raise ReleaseBuildError, "An error occured building the release, make command returned: %s" % (e)
def _doBuild(self, buildname, log):
makeOptions = self.defaultMakeOptions.copy()
makeOptions['CHROOTDIR'] = self.chroot
makeOptions['CVSROOT'] = self.cvsroot
makeOptions['RELEASETAG'] = self.cvstag
makeOptions['BUILDNAME'] = buildname
if (self.makecds == True):
makeOptions['MAKE_ISOS'] = 'yes'
makecmd = MakeCommand(FREEBSD_REL_PATH, self.makeTarget, makeOptions)
d = makecmd.make(log)
return d
def build(self, log):
"""
Build the release
@param log: Open log file
@return Returns a deferred that will be called when make(1) completes
"""
# Grab the correct buildname from CVS
d = defer.Deferred()
pp = NCVSBuildnameProcessProtocol(d)
# Kick off the build once we get the release name from CVS
d.addCallback(self._doBuild, log)
d.addErrback(self._ebBuildError)
reactor.spawnProcess(pp, CVS_PATH, args=[CVS_PATH, '-R', '-d', self.cvsroot, 'co', '-p', '-r', self.cvstag, NEWVERS_PATH], env=ROOT_ENV)
return d
class ISOReader(object):
"""
Copy a binary FreeBSD release from a mounted CD image into a build chroot's
RELEASE_CD_PATH.
"""
def __init__(self, mountpoint, releaseroot):
"""
Create a new ISOReader instance
@param mountpoint: Mount point of FreeBSD install ISO
@param releaseroot Release directory to copy the release to. The full
contents of the ISO are copied to RELEASE_CD_PATH in this directory
"""
self.mountpoint = mountpoint
self.releaseroot = releaseroot
self.cdroot = os.path.join(self.releaseroot, RELEASE_CD_PATH)
def _ebCopy(self, failure):
try:
failure.raiseException()
except ChrootCleanerError, e:
raise ISOReaderError, "Error cleaning chroot %s: %s" % (self.releaseroot, e)
except utils.Error, e:
raise ISOReaderError, "Error copying contents of ISO at %s to %s: %s" % (self.mountpoint, self.cdroot, e)
def copy(self, log):
"""
Copy release from ISO to target directory.
@param log: Open log file
"""
# Clean out release
cc = ChrootCleaner(self.releaseroot)
d = cc.clean(log)
# Now do the copy
d.addCallback(lambda _: threads.deferToThread(utils.copyRecursive, self.mountpoint, self.cdroot, symlinks=True))
d.addErrback(self._ebCopy)
return d
class PackageChrootAssembler(object):
"""
Extract release binaries into a chroot in which packages can be built.
"""
def __init__(self, releaseroot, chroot):
"""
Create a new PackageChrootAssembler instance
@param releaseroot: Directory that contains built release in R/
@param chroot: Chroot directory to install to
"""
self.cdroot = os.path.join(releaseroot, RELEASE_CD_PATH)
self.chroot = chroot
def _ebExtractError(self, failure):
try:
failure.raiseException()
except ChrootCleanerError, e:
raise PackageChrootAssemblerError, "Error cleaning chroot %s: %s" % (self.chroot, e)
except Exception, e:
raise PackageChrootAssemblerError, "Error extracting release from %s to %s: %s" % (self.cdroot, self.chroot, e)
def _cbExtractDist(self, distdir, distname, target, log):
# Extract a distribution set into the chroot with tar.
d = defer.Deferred()
protocol = LoggingProcessProtocol(d, log)
argv = [TAR_PATH, '--unlink', '-xpvzf', '-', '-C', target]
# Create target directory if it isn't already there
if (not os.path.exists(target)):
os.makedirs(target)
# Spawn a process to run tar, keeping the IProcessTransport
# providing object it returns so we can write to the process'
# stdin using IProcessTransport.writeToChild()
pt = reactor.spawnProcess(protocol, TAR_PATH, args=argv, env=ROOT_ENV)
path = os.path.join(distdir, distname)
files = glob.glob(path + '.??')
for filename in files:
file = open(filename, 'rb')
pt.writeToChild(0, file.read())
file.close()
pt.closeStdin()
return d
def _cbExtractAll(self, clean, dists, log):
# The clean argument is what is returned by the chroot cleaner. It
# should be None, and doesn't seem necessary to worry about
deferreds = []
# Extract each dist in the chroot
for key in dists.iterkeys():
distdir = os.path.join(self.cdroot, os.path.join(self.cdroot, _getCDRelease(self.cdroot)), key)
for distname in dists[key]:
# Just to make things difficult, not all dists extract relative
# to /. The source distribution sets, for instance, should be
# extracted in /usr/src/.
# TODO: I'd love to handle this a better way, and I don't know
# what releases of FreeBSD are this way (all of them?). It
# would be a good idea to handle other distribution sets that
# have this fun property (e.g. kernels extract to /boot). See
# Distribution structs near the top of
# /usr/src/usr.sbin/sysinstall/dist.c
if key == 'src':
target = os.path.join(self.chroot, 'usr', 'src')
else:
target = self.chroot
deferreds.append(self._cbExtractDist(distdir, distname, target, log))
d = defer.DeferredList(deferreds, fireOnOneErrback=True)
return d
def extract(self, dists, log):
"""
Extract the release into a chroot
@param dists: Dictionary of distribution sets to extract. The key is a
string corresponding to the name of the dist, and the value is an
array of the subdists in that directory. For dists that don't have
a lot of subdists, this value will probably just be an array
containing the same string as the key.
@param log: Open log file
"""
# Clean out chroot
cc = ChrootCleaner(self.chroot)
d = cc.clean(log)
# Then extract all dists
d.addCallback(self._cbExtractAll, dists, log)
# Add /etc/resolv.conf to chroot
d.addCallback(lambda _: threads.deferToThread(utils.copyWithOwnership, os.path.join(ROOT_PATH, RESOLV_CONF), os.path.join(self.chroot, RESOLV_CONF)))
d.addErrback(self._ebExtractError)
return d
class PackageBuilder(object):
"""
Build a package from a FreeBSD port
"""
makeTarget = ('deinstall', 'clean', 'package-recursive')
defaultMakeOptions = {
'PACKAGE_BUILDING' : 'yes',
'BATCH' : 'yes',
'NOCLEANDEPENDS' : 'yes'
}
"""
Build a FreeBSD Package
"""
def __init__(self, pkgroot, port, buildOptions=None):
"""
Create a new PackageBuilder instance.
@param pkgroot: Chroot directory where packages will be built
@param port: Port to build
@param buildOptions: Build options for the package
"""
self.pkgroot = pkgroot
self.port = port
self.buildOptions = buildOptions
def _ebBuildError(self, failure):
try:
failure.raiseException()
except MakeCommandError, e:
raise PackageBuildError, "An error occured building the port \"%s\", make command returned: %s" % (self.port, e)
def build(self, log):
"""
Build the package
@param log: Open log file
@return Returns a deferred that will be called when make(1) completes
"""
# Load up a deferred with the right call backs and return it
# ready to be spawned
makeOptions = self.defaultMakeOptions.copy()
makeOptions.update(self.buildOptions)
makecmd = MakeCommand(os.path.join(FREEBSD_PORTS_PATH, self.port), self.makeTarget, makeOptions, self.pkgroot)
d = makecmd.make(log)
d.addErrback(self._ebBuildError)
return d
class InstallAssembler(object):
"""
Assemble an installation configuration
"""
def __init__(self, name, description, releaseroot, installConfigPath):
"""
@param name: A unique name for this install instance
@param description: A human-readable description of this install type
@param releaseroot: Directory containing the release binaries
@param installConfigFile: The complete path to this installation's install.cfg
"""
self.name = name
self.description = description
self.releaseroot = releaseroot
self.installConfigSource = installConfigPath
#
# Source Paths
#
# Contains shared release boot files
self.bootRoot = os.path.join(self.releaseroot, RELEASE_CD_PATH, 'boot')
# Shared release mfsroot
self.mfsCompressed = os.path.join(self.bootRoot, 'mfsroot.gz')
# Directory containing generic kernel and its modules
self.kernel = os.path.join(self.bootRoot, 'kernel')
def _ebInstallError(self, failure):
try:
failure.raiseException()
except MDConfigCommandError, e:
raise InstallAssembleError, "An error occured operating on the mfsroot \"%s\": %s" % (self.mfsOutput, e)
except MountCommandError, e:
raise InstallAssembleError, "An error occured mounting \"%s\": %s" % (self.mfsOutput, e)
except exceptions.IOError, e:
raise InstallAssembleError, "An I/O error occured: %s" % e
except Exception, e:
raise InstallAssembleError, "An error occured: %s" % e
def _decompressMFSRoot(self, mfsOutput):
"""
Synchronous decompression/writing of mfsroot file
(Not worth making async, so run in a thread)
"""
compressedFile = gzip.GzipFile(self.mfsCompressed, 'rb')
outputFile = open(mfsOutput, 'wb')
while (True):
data = compressedFile.read(1024)
if (not data):
break
outputFile.write(data)
return mfsOutput
def _cbMountMFSRoot(self, mfsOutput, mountPoint, log):
"""
Once the MFS root has been decompressed,
mount it
"""
mdconfig = MDConfigCommand(mfsOutput)
# Create the mount point, if necessary
if (not os.path.exists(mountPoint)):
os.mkdir(mountPoint)
self.mdmount = MDMountCommand(mdconfig, mountPoint)
return self.mdmount.mount(log)
def _cbCopyKernel(self, result, destdir):
"""
Copy the kernel directory to the install-specific directory
(Synchronous)
"""
dest = os.path.join(destdir, 'kernel')
d = threads.deferToThread(utils.copyRecursive, self.kernel, dest, symlinks=True)
return d
def _doWriteBootConf(self, destdir):
"""
Write the per-install bootloader configuration file
"""
subst = {}
subst['bootdir'] = os.path.basename(destdir)
output = open(os.path.join(destdir, 'boot.conf'), 'w')
template = open(farb.BOOT_CONF_TMPL, 'r')
for line in template:
output.write(line % (subst))
output.close()
template.close()
def build(self, destdir, log):
"""
Build the MFSRoot, build the boot loader configuration, and copy the kernel.
@param destdir: The installation-specific boot-loader directory
@param log: Open log file
@return Returns a deferred
"""
#
# Destination Paths
#
# Path to installation-specific mfsroot
mfsOutput = os.path.join(destdir, "mfsroot")
# Temporary mount point for the mfsroot image
mountPoint = os.path.join(destdir, "mnt")
# Write the install.cfg to the mfsroot mount point
installConfigDest = os.path.join(mountPoint, 'install.cfg')
# Create the destdir, if necessary
if (not os.path.exists(destdir)):
os.mkdir(destdir)
# Write the uncompressed mfsroot file
d = threads.deferToThread(self._decompressMFSRoot, mfsOutput)
# Mount the mfsroot once it has been decompressed
d.addErrback(self._ebInstallError)
d.addCallback(self._cbMountMFSRoot, mountPoint, log)
# Copy the install.cfg to the attached md device
d.addCallback(lambda _: shutil.copy2(self.installConfigSource, installConfigDest))
# Unmount/detach md device
d.addCallback(lambda _: self.mdmount.umount(log))
# Copy the kernel
d.addCallback(self._cbCopyKernel, destdir)
# Write boot.conf
d.addCallback(lambda _: threads.deferToThread(self._doWriteBootConf, destdir))
return d
class ReleaseAssembler(object):
"""
Assemble the per-release installation data directory.
"""
def __init__(self, name, releaseroot, pkgroot, localData = []):
"""
Initialize the ReleaseAssembler
@param name: A unique name for this release
@param releaseroot: Directory containing the release binaries
@param pkgroot: Chroot directory where packages were built
@param localData: List of file and directory paths to copy to installRoot/local.
"""
self.name = name
self.cdroot = os.path.join(releaseroot, RELEASE_CD_PATH)
self.pkgroot = pkgroot
self.localData = localData
def _cbCopyLocal(self, result, source, dest):
if (os.path.isdir(source)):
d = threads.deferToThread(utils.copyRecursive, source, os.path.join(dest, os.path.basename(source)), symlinks=True)
else:
d = threads.deferToThread(utils.copyWithOwnership, source, dest)
return d
def _ebBuild(self, failure):
try:
failure.raiseException()
except exceptions.IOError, e:
raise ReleaseAssembleError, "An I/O error occured: %s" % e
except Exception, e:
raise ReleaseAssembleError, "An error occured: %s" % e
def build(self, destdir, log):
"""
Create the install root, copy in the release data,
write out the bootloader configuration and kernels.
@param destdir: Per-release installation data directory.
@param log: Open log file.
"""
# Copy the installation data
d = threads.deferToThread(utils.copyRecursive, os.path.join(self.cdroot, _getCDRelease(self.cdroot)), destdir, symlinks=True)
# If there are packages, copy those too
packagedir = os.path.join(self.pkgroot, RELEASE_PACKAGE_PATH)
if (os.path.exists(packagedir)):
d.addCallback(lambda _: threads.deferToThread(utils.copyRecursive, packagedir, os.path.join(destdir, 'packages'), symlinks=True))
# Copy in any local data
if (len(self.localData)):
# Create the local directory
localdir = os.path.join(destdir, 'local')
d.addCallback(lambda _: os.mkdir(localdir))
for path in self.localData:
d.addCallback(self._cbCopyLocal, path, localdir)
# Add the FarBot package installer script and make it executable
d.addCallback(lambda _: threads.deferToThread(utils.copyWithOwnership, farb.INSTALL_PACKAGE_SH, destdir))
d.addCallback(lambda _: os.chmod(os.path.join(destdir, os.path.basename(farb.INSTALL_PACKAGE_SH)), 0755))
return d
class NetInstallAssembler(object):
"""
Assemble the netinstall directory, including the tftproot,
using the supplied release and install assemblers.
"""
def __init__(self, installroot, releaseAssemblers, installAssemblers):
"""
Initialize the InstallRootBuilder
@param installroot: Network install/boot directory.
@param releaseAssemblers: List of ReleaseAssembler instances.
@param installAssemblers: List of InstallAssembler instances.
"""
self.installroot = installroot
self.tftproot = os.path.join(installroot, 'tftproot')
self.releaseAssemblers = releaseAssemblers
self.installAssemblers = installAssemblers
def _ebBuild(self, failure):
"""
Called if any deferred in the DeferredList
fails. Handles the original exception.
"""
try:
failure.value.subFailure.raiseException()
except exceptions.IOError, e:
raise NetInstallAssembleError, "An I/O error occured: %s" % e
except exceptions.OSError, e:
raise NetInstallAssembleError, "An OS error occured: %s" % e
except Exception, e:
raise NetInstallAssembleError, "An error occured: %s" % e
def _doConfigureBootLoader(self, destdir):
"""
Write out the forth for the boot loader installation menu
"""
subst = {}
# Format Strings
variableFormat = 'variable %s\n'
menuItemFormat = 'printmenuitem ." %s" %s !\n'
ifBlockFormat = 'dup %s @ = if\ns" /%s/boot.conf" read-conf\n0 boot-conf exit\nthen\n'
# Output
variables = cStringIO.StringIO()
menuItems = cStringIO.StringIO()
ifBlocks = cStringIO.StringIO()
# Generate the code blocks
for install in self.installAssemblers:
# Variable declaration
variableName = install.name + '_key'
variables.write(variableFormat % (variableName))
# Menu item
menuItems.write(menuItemFormat % (install.description, variableName))
# if block
ifBlocks.write(ifBlockFormat % (variableName, install.name))
# Write out the netinstall.4th file
subst['variables'] = variables.getvalue()
subst['menuitems'] = menuItems.getvalue()
subst['ifblocks'] = ifBlocks.getvalue()
output = open(os.path.join(destdir, 'netinstall.4th'), 'w')
template = open(farb.NETINSTALL_FORTH_TMPL, 'r')
for line in template:
output.write(line % (subst))
output.close()
template.close()
# Copy in our loader.conf and loader.rc
utils.copyWithOwnership(farb.LOADER_CONF, destdir)
utils.copyWithOwnership(farb.LOADER_RC, destdir)
def build(self, log):
"""
Create the install root, copy in the release data,
write out the bootloader configuration and kernels.
@param log: Open log file.
"""
deferreds = []
# Create the installation root, if necessary
if (not os.path.exists(self.installroot)):
os.mkdir(self.installroot)
# Create the tftproot, if necessary
if (not os.path.exists(self.tftproot)):
os.mkdir(self.tftproot)
# Copy over the shared boot loader and kernel. Lacking any better heuristic, we
# grab the boot loader from the first release provided -- shouldn't
# matter where we get it, really. However, there are some differences between
# where releases store the generic kernel, so we try to impedence match.
release = self.releaseAssemblers[0]
source = os.path.join(release.cdroot, 'boot')
dest = os.path.join(self.tftproot, os.path.basename(source))
# Copy it
d = threads.deferToThread(utils.copyRecursive, source, dest, symlinks=True)
# Configure it
d.addCallback(lambda _: threads.deferToThread(self._doConfigureBootLoader, dest))
deferreds.append(d)
# Assemble the release data
for release in self.releaseAssemblers:
destdir = os.path.join(self.installroot, release.name)
d = release.build(destdir, log)
deferreds.append(d)
# Assemble the installation data
for install in self.installAssemblers:
destdir = os.path.join(self.tftproot, install.name)
d = install.build(destdir, log)
deferreds.append(d)
d = defer.DeferredList(deferreds, fireOnOneErrback=True)
d.addErrback(self._ebBuild)
return d
def _getCDRelease(cdroot):
# Get the release name from the cdrom.inf file in cdroot
infFile = os.path.join(cdroot, 'cdrom.inf')
if not os.path.exists(infFile):
raise CDReleaseError, "No cdrom.inf file in %s. Is this a disc 1 root directory for FreeBSD >= 2.1.5?" % (cdroot)
# First line in cdrom.inf should look like: CD_VERSION = x.y-RELEASE
fileObj = open(infFile, 'r')
line = fileObj.readline()
fileObj.close()
line = line.strip()
splitString = line.split(' = ')
if (len(splitString) != 2 or splitString[0] != 'CD_VERSION'):
raise CDReleaseError, "cdrom.inf file in %s has unrecognized first line: %s" % (cdroot, line)
return splitString[1]
syntax highlighted by Code2HTML, v. 0.9.1