diff options
Diffstat (limited to 'portage/ebuild/ebuild_internal.py')
-rw-r--r-- | portage/ebuild/ebuild_internal.py | 1265 |
1 files changed, 1265 insertions, 0 deletions
diff --git a/portage/ebuild/ebuild_internal.py b/portage/ebuild/ebuild_internal.py new file mode 100644 index 0000000..7f86430 --- /dev/null +++ b/portage/ebuild/ebuild_internal.py @@ -0,0 +1,1265 @@ +#!/usr/bin/python +# ebuild.py; Ebuild classes/abstraction of phase processing, and communicating with a ebuild-daemon.sh instance +# Copyright 2004 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +#$Header: /local/data/ulm/cvs/history/var/cvsroot/gentoo-src/portage/portage/ebuild/ebuild_internal.py,v 1.1 2005/07/10 09:21:05 ferringb Exp $ + + +import os,sys,traceback +import portage_const,types +#still needed? +from portage_const import * +import portage_locks, portage_util +import portage_exec +import portage_versions +import shutil, anydbm +import stat +import string + +def shutdown_all_processors(): + """kill off all known processors""" + global active_ebp_list, inactive_ebp_list + if type(active_ebp_list) != types.ListType: + print "warning, ebuild.active_ebp_list wasn't a list." + active_ebp_list = [] + if type(inactive_ebp_list) != types.ListType: + print "warning, ebuild.inactive_ebp_list wasn't a list." + inactive_ebp_list = [] + while len(active_ebp_list) > 0: + try: active_ebp_list[0].shutdown_processor() + except (IOError,OSError): + active_ebp_list.pop(0) + continue + try: active_ebp_list.pop(0) + except IndexError: pass + while len(inactive_ebp_list) > 0: + try: + inactive_ebp_list[0].shutdown_processor() + except (IOError,OSError): + inactive_ebp_list.pop(0) + continue + try: inactive_ebp_list.pop(0) + except IndexError: pass + + +inactive_ebp_list = [] +active_ebp_list = [] + +def request_ebuild_processor(config,ebuild_daemon_path=portage_const.EBUILD_DAEMON_PATH,userpriv=False, \ + sandbox=None,fakeroot=False,save_file=None): + """request an ebuild_processor instance from the pool, or create a new one + this walks through the requirements, matching a inactive processor if one exists + note fakerooted processors are never reused, do to the nature of fakeroot""" + + if sandbox == None: + sandbox = portage_exec.sandbox_capable + + global inactive_ebp_list, active_ebp_list + if not fakeroot: + for x in inactive_ebp_list: + if not x.locked() and x.ebd == ebuild_daemon_path and \ + x.userprived() == userpriv and (x.sandboxed() or not sandbox): + inactive_ebp_list.remove(x) + active_ebp_list.append(x) + return x + active_ebp_list.append(ebuild_processor(config, userpriv=userpriv,sandbox=sandbox,fakeroot=fakeroot,save_file=save_file)) + return active_ebp_list[-1] + +def release_ebuild_processor(ebp): + """the inverse of request_ebuild_processor. Any processor requested via request_ebuild_processor + _must_ be released via this function once it's no longer in use. + this includes fakerooted processors. + Returns True exempting when the processor requested to be released isn't marked as active""" + + global inactive_ebp_list, active_ebp_list + try: active_ebp_list.remove(ebp) + except ValueError: return False + + try: inactive_ebp_list.index(ebp) + except ValueError: + # if it's a fakeroot'd process, we throw it away. it's not useful outside of a chain of calls + if not ebp.onetime(): + inactive_ebp_list.append(ebp) + else: + del ebp + return True + + # if it makes it this far, that means ebp was already in the inactive list. + # which is indicative of an internal fsck up. + import traceback + print "ebp was requested to be free'd, yet it already is claimed inactive _and_ was in the active list" + print "this means somethings horked, badly" + traceback.print_stack() + return False + + + +class ebuild_processor: + """abstraction of a running ebuild.sh instance- the env, functions, etc that ebuilds expect.""" + def __init__(self, config, ebuild_daemon_path=portage_const.EBUILD_DAEMON_PATH,userpriv=False, sandbox=True, \ + fakeroot=False,save_file=None): + """ebuild_daemon_path shouldn't be fooled with unless the caller knows what they're doing. + sandbox enables a sandboxed processor + userpriv enables a userpriv'd processor + fakeroot enables a fakeroot'd processor- this is a mutually exclusive option to sandbox, and + requires userpriv to be enabled. Violating this will result in nastyness""" + + self._config = config + self.ebd = ebuild_daemon_path + from portage_data import portage_uid, portage_gid + spawn_opts = {} + + if fakeroot and (sandbox or not userpriv): + import traceback + traceback.print_stack() + print "warning, was asking to enable fakeroot but-" + print "sandbox",sandbox,"userpriv",userpriv + print "this isn't valid. bailing" + raise Exception,"cannot initialize with sandbox and fakeroot" + + if userpriv: + self.__userpriv = True + spawn_opts.update({"uid":portage_uid,"gid":portage_gid,"groups":[portage_gid],"umask":002}) + else: + if portage_exec.userpriv_capable: + spawn_opts.update({"gid":portage_gid,"groups":[0,portage_gid]}) + self.__userpriv = False + + # open the pipes to be used for chatting with the new daemon + cread, cwrite = os.pipe() + dread, dwrite = os.pipe() + self.__sandbox = False + self.__fakeroot = False + + # since it's questionable which spawn method we'll use (if sandbox or fakeroot fex), + # we ensure the bashrc is invalid. + env={"BASHRC":"/etc/portage/spork/not/valid/ha/ha"} + args = [] + if sandbox: + if fakeroot: + print "!!! ERROR: fakeroot was on, but sandbox was also on" + sys.exit(1) + self.__sandbox = True + spawn_func = portage_exec.spawn_sandbox + env.update({"SANDBOX_DEBUG":"1","SANDBOX_DEBUG_LOG":"/var/tmp/test"}) + + elif fakeroot: + self.__fakeroot = True + spawn_func = portage_exec.spawn_fakeroot + args.append(save_file) + else: + spawn_func = portage_exec.spawn + + self.pid = spawn_func(self.ebd+" daemonize", fd_pipes={0:0, 1:1, 2:2, 3:cread, 4:dwrite}, + returnpid=True,env=env, *args, **spawn_opts)[0] + + os.close(cread) + os.close(dwrite) + self.ebd_write = os.fdopen(cwrite,"w") + self.ebd_read = os.fdopen(dread,"r") + + # basically a quick "yo" to the daemon + self.write("dude?") + if not self.expect("dude!"): + print "error in server coms, bailing." + raise Exception("expected 'dude!' response from ebd, which wasn't received. likely a bug") + if self.__sandbox: + self.write("sandbox_log?") + self.__sandbox_log = self.read().split()[0] + self.dont_export_vars=self.read().split() + # locking isn't used much, but w/ threading this will matter + self.unlock() + + def sandboxed(self): + """is this instance sandboxed?""" + return self.__sandbox + + def userprived(self): + """is this instance userprived?""" + return self.__userpriv + + def fakerooted(self): + """is this instance fakerooted?""" + return self.__fakeroot + + def onetime(self): + """is this instance going to be discarded after usage; eg is it fakerooted?""" + return self.__fakeroot + + def write(self, string,flush=True): + """talk to running daemon. Disabling flush is useful when dumping large amounts of data + all strings written are automatically \\n terminated""" + if string[-1] == "\n": + self.ebd_write.write(string) + else: + self.ebd_write.write(string +"\n") + if flush: + self.ebd_write.flush() + + def expect(self, want): + """read from the daemon, and return true or false if the returned string is what is expected""" + got=self.ebd_read.readline() + return want==got[:-1] + + def read(self,lines=1): + """read data from the daemon. Shouldn't be called except internally""" + mydata='' + while lines > 0: + mydata += self.ebd_read.readline() + lines -= 1 + return mydata + + def sandbox_summary(self, move_log=False): + """if the instance is sandboxed, print the sandbox access summary""" + if not os.path.exists(self.__sandbox_log): + self.write("end_sandbox_summary") + return 0 + violations=portage_util.grabfile(self.__sandbox_log) + if len(violations)==0: + self.write("end_sandbox_summary") + return 0 + if not move_log: + move_log=self.__sandbox_log + elif move_log != self.__sandbox_log: + myf=open(move_log) + for x in violations: + myf.write(x+"\n") + myf.close() + from output import red + self.ebd_write.write(red("--------------------------- ACCESS VIOLATION SUMMARY ---------------------------")+"\n") + self.ebd_write.write(red("LOG FILE = \"%s\"" % move_log)+"\n\n") + for x in violations: + self.ebd_write.write(x+"\n") + self.write(red("--------------------------------------------------------------------------------")+"\n") + self.write("end_sandbox_summary") + try: + os.remove(self.__sandbox_log) + except (IOError, OSError), e: + print "exception caught when cleansing sandbox_log=%s" % str(e) + return 1 + + def preload_eclasses(self, ec_file): + """this preloades eclasses into a function, thus avoiding the cost of going to disk. + preloading eutils (which is heaviliy inherited) speeds up regen times fex""" + if not os.path.exists(ec_file): + return 1 + self.write("preload_eclass %s" % ec_file) + if self.expect("preload_eclass succeeded"): + self.preloaded_eclasses=True + return True + return False + + def lock(self): + """lock the processor. Currently doesn't block any access, but will""" + self.processing_lock = True + + def unlock(self): + """unlock the processor""" + self.processing_lock = False + + def locked(self): + """is the processor locked?""" + return self.processing_lock + + def is_alive(self): + """returns if it's known if the processor has been shutdown. + Currently doesn't check to ensure the pid is still running, yet it should""" + return self.pid != None + + def shutdown_processor(self): + """tell the daemon to shut itself down, and mark this instance as dead""" + try: + if self.is_alive(): + self.write("shutdown_daemon") + self.ebd_write.close() + self.ebd_read.close() + + # now we wait. + os.waitpid(self.pid,0) + except (IOError,OSError,ValueError): + pass + + # we *really* ought to modify portageatexit so that we can set limits for waitpid. + # currently, this assumes all went well. + # which isn't always true. + self.pid = None + + def set_sandbox_state(self,state): + """tell the daemon whether to enable the sandbox, or disable it""" + if state: + self.write("set_sandbox_state 1") + else: + self.write("set_sandbox_state 0") + + def send_env(self): + """essentially transfer the ebuild's desired env to the running daemon + accepts a portage.config instance, although it will accept dicts at some point""" + be=self._config.bash_environ() + self.write("start_receiving_env\n") + exported_keys = '' + for x in be.keys(): + if x not in self.dont_export_vars: + self.write("%s=%s\n" % (x,be[x]), flush=False) + exported_keys += x+' ' + self.write("export "+exported_keys,flush=False) + self.write("end_receiving_env") + return self.expect("env_received") + + def set_logfile(self,logfile=''): + """relevant only when the daemon is sandbox'd, set the logfile""" + self.write("logging %s" % logfile) + return self.expect("logging_ack") + + + def __del__(self): + """simply attempts to notify the daemon to die""" + # for this to be reached means we ain't in a list no more. + if self.pid: + self.shutdown_processor() + + +class ebuild_handler: + """abstraction of ebuild phases, fetching exported keys, fetching srcs, etc""" + import portageq + def __init__(self, config, process_limit=5): + """process_limit is currently ignored""" + self.processed = 0 + self.__process_limit = process_limit + self.preloaded_eclasses = False + self._config = config + self.__ebp = None + + def __del__(self): + """only ensures any processors this handler has claimed are released""" + if self.__ebp: + release_ebuild_processor(self.__ebp) + + # this is an implementation of stuart's confcache/sandbox trickery, basically the file/md5 stuff implemented in + # python with a basic bash wrapper that calls back to this. + # all credit for the approach goes to him, as stated, this is just an implementation of it. + # bugs should be thrown at ferringb. + def load_confcache(self,transfer_to,confcache=portage_const.CONFCACHE_FILE, + confcache_list=portage_const.CONFCACHE_LIST): + """verifys a requested conf cache, removing the global cache if it's stale. + The handler should be the only one to call this""" + from portage_checksum import perform_md5 + from output import red + if not self.__ebp: + import traceback + traceback.print_stack() + print "err... no ebp, yet load_confcache called. invalid" + raise Exception,"load_confcache called yet no running processor. bug?" + + valid=True + lock=None + if not os.path.exists(confcache_list): + print "confcache file listing doesn't exist" + valid=False + elif not os.path.exists(confcache): + print "confcache doesn't exist" + valid=False + else: + lock=portage_locks.lockfile(confcache_list,wantnewlockfile=1) + try: + myf=anydbm.open(confcache_list, "r", 0664) + for l in myf.keys(): + # file, md5 + if perform_md5(l,calc_prelink=1) != myf[l]: + print red("***")+" confcache is stale: %s: recorded md5: %s: actual: %s:" % (l,myf[l],perform_md5(l,calc_prelink=1)) + raise Exception("md5 didn't match") + myf.close() + # verify env now. + new_cache=[] + env_vars=[] + + # guessing on THOST. I'm sure it's wrong... + + env_translate={"build_alias":"CBUILD","host_alias":"CHOST","target_alias":"THOST"} + cache=portage_util.grabfile(confcache) + + x=0 + while x < len(cache): + #ac_cv_env + if cache[x][0:10] == "ac_cv_env_": + f=cache[x][10:].find("_set") + if f == -1 or f==11: + cache.pop(x) + continue + env_vars.append(cache[x][10:10 + cache[x][10:].find("_set")]) + x += 1 + else: + new_cache.append(cache[x]) + x += 1 + + for x in env_vars: + self.__ebp.write("request %s" % env_translate.get(x,x)) + line=self.__ebp.read() + if line[-1] == "\n": + line=line[:-1] + new_cache.append("ac_cv_env_%s_set=%s" % (x, line)) + if line == "unset": + new_cache.append("ac_cv_env_%s_value=" % x) + else: + line=self.__ebp.read() + if line[-1] == "\n": + line=line[:-1] + if line.split()[0] != line: + #quoting... XXX + new_cache.append("ac_cv_env_%s_value='%s'" % (x,line)) + else: + new_cache.append("ac_cv_env_%s_value=%s" % (x,line)) + + myf=open(confcache,"w") + for x in new_cache: + myf.write(x+"\n") + myf.close() + + except SystemExit, e: + raise + except Exception,e: + print "caught exception: %s" % str(e) + try: myf.close() + except (IOError, OSError): pass + valid=False + + if not valid: + print "\nconfcache is invalid\n" + try: os.remove(confcache_list) + except OSError: pass + try: os.remove(confcache) + except OSError: pass + self.__ebp.write("empty") + valid=0 + else: + self.__ebp.write("location: %s" % confcache) + valid=1 + if lock: + portage_locks.unlockfile(lock) + return valid + + def update_confcache(self,settings,logfile,new_confcache, confcache=portage_const.CONFCACHE_FILE, \ + confcache_list=portage_const.CONFCACHE_LIST): + """internal function called when a processor has finished a configure, and wishes its cache + be transferred to the global cache + This runs through the sandbox log, storing the md5 of files along with the list of files to check. + Finally, it transfers the cache to the global location.""" + + if not self.__ebp: + import traceback + traceback.print_stack() + print "err... no ebp, yet load_confcache called. invalid" + sys.exit(1) + + import re + from portage_checksum import perform_md5 + if not (os.path.exists(logfile) and os.path.exists(new_confcache)) : + # eh? wth? + self.__ebp.write("failed") + return 0 + myfiles=portage_util.grabfile(logfile) + filter=re.compile('^(%s|/tmp|/dev|.*/\.ccache)/' % os.path.normpath(settings["PORTAGE_TMPDIR"])) + l=[] + for x in myfiles: + # get only read syscalls... + if x[0:8] == "open_rd:": + l.append(x.split()[1]) + + myfiles = portage_util.unique_array(l) + l=[] + for x in myfiles: + if not os.path.exists(x): + continue + if not filter.match(x): + l.append(x) + del myfiles + + if not len(l): + self.__ebp.write("updated") + return 0 + + lock=portage_locks.lockfile(confcache_list,wantnewlockfile=1) + # update phase. + if not os.path.exists(confcache_list): + prevmask=os.umask(0) + myf=anydbm.open(confcache_list,"n",0664) + os.umask(prevmask) + else: + myf=anydbm.open(confcache_list,"w",0664) + + for x in l: + try: + if not stat.S_ISDIR(os.stat(x).st_mode) and not myf.has_key(x): + myf[x]=str(perform_md5(x,calc_prelink=1)) + except (IOError, OSError): + # exceptions are only possibly (ignoring anydbm horkage) from os.stat + pass + myf.close() + from portage_data import portage_gid + os.chown(confcache_list, -1, portage_gid) + shutil.move(new_confcache, confcache) + os.chown(confcache, -1, portage_gid) + m=os.umask(0) + os.chmod(confcache, 0664) + os.chmod(confcache_list, 0664) + os.umask(m) + portage_locks.unlockfile(lock) + self.__ebp.write("updated") + return 0 + + def get_keys(self,myebuild,myroot="/"): + """request the auxdbkeys from an ebuild + returns a dict when successful, None when failed""" +# print "getting keys for %s" % myebuild + # normally, + # userpriv'd, minus sandbox. which is odd. + # I say both, personally (and I'm writing it, so live with it) + if self.__ebp: + import traceback + traceback.print_stack() + print "self.__ebp exists. it shouldn't. this indicates a handler w/ an active ebp never" + print "released it, or a bug in the calls" + sys.exit(1) + + + self.__ebp = request_ebuild_processor(self._config, userpriv=portage_exec.userpriv_capable) + + if self.__adjust_env("depend",myebuild,myroot): + return {} + + self.__ebp.write("process_ebuild depend") + self.__ebp.send_env() + self.__ebp.set_sandbox_state(True) + self.__ebp.write("start_processing") + line=self.__generic_phase(["sending_keys"], interpret_results=False) + if line != "sending_keys": + return None + mykeys={} + while line != "end_keys": + line=self.__ebp.read() + line=line[:-1] + if line == "failed": + self.__ebp.unlock() + return {} + if line == "end_keys" or not len(line): + continue + pair = line.split('=',1) + mykeys[pair[0]]=pair[1] + self.__ebp.expect("phases succeeded") + if not release_ebuild_processor(self.__ebp): + self.__ebp = None + raise Exception,"crud" + self.__ebp = None + return mykeys + + def __adjust_env(self,mydo,myebuild,myroot,debug=0,listonly=0,fetchonly=0,cleanup=0,dbkey=None,\ + use_cache=1,fetchall=0,tree="porttree",use_info_env=True,verbosity=0): + """formerly portage.doebuild, since it's specific to ebuilds, it's now a method of ebuild handling. + severely gutted, and in need of cleansing/exorcism""" + from portage import db,ExtractKernelVersion,fetch,features, \ + digestgen,digestcheck,root,flatten, digestParseFile + from portage_data import portage_uid,portage_gid,secpass + import portage_dep + from portage_util import writemsg + + ebuild_path = os.path.abspath(myebuild) + pkg_dir = os.path.dirname(ebuild_path) + + if self._config.configdict["pkg"].has_key("CATEGORY"): + cat = self._config.configdict["pkg"]["CATEGORY"] + else: + cat = os.path.basename(os.path.normpath(pkg_dir+"/..")) + mypv = os.path.basename(ebuild_path)[:-7] + mycpv = cat+"/"+mypv + + mysplit=portage_versions.pkgsplit(mypv,silent=0) + if mysplit==None: + writemsg("!!! Error: PF is null '%s'; exiting.\n" % mypv) + return 1 + + if mydo == "clean": + cleanup=True + + if mydo != "depend": + # XXX: We're doing a little hack here to curtain the gvisible locking + # XXX: that creates a deadlock... Really need to isolate that. + self._config.reset(use_cache=use_cache) + + self._config.setcpv(mycpv,use_cache=use_cache) + + if not os.path.exists(myebuild): + writemsg("!!! doebuild: "+str(myebuild)+" not found for "+str(mydo)+"\n") + return 1 + + if debug: # Otherwise it overrides emerge's settings. + # We have no other way to set debug... debug can't be passed in + # due to how it's coded... Don't overwrite this so we can use it. + self._config["PORTAGE_DEBUG"]=str(debug) + + self._config["ROOT"] = myroot + + self._config["EBUILD"] = ebuild_path + self._config["O"] = pkg_dir + self._config["CATEGORY"] = cat + self._config["FILESDIR"] = pkg_dir+"/files" + self._config["PF"] = mypv + + self._config["ECLASSDIR"] = self._config["PORTDIR"]+"/eclass" + + self._config["PROFILE_PATHS"] = PROFILE_PATH+"\n"+CUSTOM_PROFILE_PATH + self._config["P"] = mysplit[0]+"-"+mysplit[1] + self._config["PN"] = mysplit[0] + self._config["PV"] = mysplit[1] + self._config["PR"] = mysplit[2] + + # ensure this is set for all phases, setup included. + # Should be ok again to set $T, as sandbox does not depend on it + self._config["BUILD_PREFIX"] = self._config["PORTAGE_TMPDIR"]+"/portage" + self._config["BUILDDIR"] = self._config["BUILD_PREFIX"]+"/"+self._config["PF"] + self._config["T"] = self._config["BUILDDIR"]+"/temp" + self._config["WORKDIR"] = self._config["BUILDDIR"]+"/work" + self._config["D"] = self._config["BUILDDIR"]+"/image/" + + + # bailing now, probably horks a few things up, but neh. + # got to break a few eggs to make an omelot after all (spelling is wrong, too) :) + if mydo=="unmerge": + return 0 + + if mydo!="depend": + try: + self._config["INHERITED"],self._config["RESTRICT"] = db[root][tree].dbapi.aux_get( + mycpv,["INHERITED","RESTRICT"]) + + self._config["PORTAGE_RESTRICT"]=string.join(flatten(portage_dep.use_reduce( + portage_dep.paren_reduce(self._config["RESTRICT"]), + uselist=self._config["USE"].split() )),' ') + + except SystemExit, e: + raise + except Exception, e: + print "caught exception %s in ebd_proc:doebuild" % str(e) + self._config["RESTRICT"] = self._config["PORTAGE_RESTRICT"] = "" + pass + + + if mysplit[2] == "r0": + self._config["PVR"]=mysplit[1] + else: + self._config["PVR"]=mysplit[1]+"-"+mysplit[2] + + self._config["SLOT"]="" + + if self._config.has_key("PATH"): + mysplit=string.split(self._config["PATH"],":") + else: + mysplit=[] + + if PORTAGE_BIN_PATH not in mysplit: + self._config["PATH"]=PORTAGE_BIN_PATH+":"+self._config["PATH"] + + if tree=="bintree": + self._config["BUILD_PREFIX"] += "-pkg" + + self._config["HOME"] = self._config["BUILD_PREFIX"]+"/homedir" + self._config["PKG_TMPDIR"] = self._config["PORTAGE_TMPDIR"]+"/portage-pkg" + + if cleanup and os.path.exists(self._config["BUILDDIR"]): + print "cleansing builddir"+self._config["BUILDDIR"] + shutil.rmtree(self._config["BUILDDIR"]) + + if mydo=="clean": + # if clean, just flat out skip the rest of this crap. + return 0 + + self._config["PORTAGE_BASHRC"] = EBUILD_SH_ENV_FILE + + #set up KV variable -- DEP SPEEDUP :: Don't waste time. Keep var persistent. + + if mydo not in ["depend","fetch","digest","manifest"]: + if not self._config.has_key("KV"): + mykv,err1=ExtractKernelVersion(root+"usr/src/linux") + if mykv: + # Regular source tree + self._config["KV"]=mykv + else: + self._config["KV"]="" + + if (mydo!="depend") or not self._config.has_key("KVERS"): + myso=os.uname()[2] + self._config["KVERS"]=myso[1] + + + # get possible slot information from the deps file + if mydo=="depend": + if self._config.has_key("PORTAGE_DEBUG") and self._config["PORTAGE_DEBUG"]=="1": + # XXX: This needs to use a FD for saving the output into a file. + # XXX: Set this up through spawn + pass + writemsg("!!! DEBUG: dbkey: %s\n" % str(dbkey),2) + if dbkey: + self._config["dbkey"] = dbkey + else: + self._config["dbkey"] = self._config.depcachedir+"/aux_db_key_temp" + + return 0 + + self._config["PORTAGE_LOGFILE"]='' + logfile=None + + + #fetch/digest crap + if mydo not in ["prerm","postrm","preinst","postinst","config","help","setup","unmerge"]: + + newuris, alist = db["/"]["porttree"].dbapi.getfetchlist(mycpv,mysetting=self._config) + alluris, aalist = db["/"]["porttree"].dbapi.getfetchlist(mycpv,mysettings=self._config,all=1) + self._config["A"]=string.join(alist," ") + self._config["AA"]=string.join(aalist," ") + if ("mirror" in features) or fetchall: + fetchme=alluris[:] + checkme=aalist[:] + elif mydo=="digest": + fetchme=alluris[:] + checkme=aalist[:] + digestfn=self._config["FILESDIR"]+"/digest-"+self._config["PF"] + if os.path.exists(digestfn): + mydigests=digestParseFile(digestfn) + if mydigests: + for x in mydigests: + while x in checkme: + i = checkme.index(x) + del fetchme[i] + del checkme[i] + else: + fetchme=newuris[:] + checkme=alist[:] + + try: + if not os.path.exists(self._config["DISTDIR"]): + os.makedirs(self._config["DISTDIR"]) + if not os.path.exists(self._config["DISTDIR"]+"/cvs-src"): + os.makedirs(self._config["DISTDIR"]+"/cvs-src") + except OSError, e: + print "!!! File system problem. (Bad Symlink?)" + print "!!! Fetching may fail:",str(e) + + try: + mystat=os.stat(self._config["DISTDIR"]+"/cvs-src") + if ((mystat[stat.ST_GID]!=portage_gid) or ((mystat[stat.ST_MODE]&00775)!=00775)) and not listonly: + print "*** Adjusting cvs-src permissions for portage user..." + os.chown(self._config["DISTDIR"]+"/cvs-src",0,portage_gid) + os.chmod(self._config["DISTDIR"]+"/cvs-src",00775) + portage_exec.spawn("chgrp -R "+str(portage_gid)+" "+self._config["DISTDIR"]+"/cvs-src") + portage_exec.spawn("chmod -R g+rw "+self._config["DISTDIR"]+"/cvs-src") + except (IOError, OSError): + pass + + if not fetch(fetchme, self._config, listonly=listonly, fetchonly=fetchonly,verbosity=verbosity): + return 1 + + if mydo=="fetch" and listonly: + return 0 + + if "digest" in features: + #generate digest if it doesn't exist. + if mydo=="digest": + # exemption to the return rule + return (not digestgen(aalist,self._config,overwrite=1,verbosity=verbosity)) + else: + digestgen(aalist,self._config,overwrite=0,verbosity=verbosity) + + elif mydo=="digest": + #since we are calling "digest" directly, recreate the digest even if it already exists + return (not digestgen(checkme,self._config,overwrite=1,verbosity=verbosity)) + if mydo=="manifest": + return (not digestgen(checkme,self._config,overwrite=1,manifestonly=1,verbosity=verbosity)) + + if not digestcheck(checkme, self._config, ("strict" in features),verbosity=verbosity): + return 1 + + if mydo=="fetch": + return 0 + + if not os.path.exists(self._config["BUILD_PREFIX"]): + os.makedirs(self._config["BUILD_PREFIX"]) + os.chown(self._config["BUILD_PREFIX"],portage_uid,portage_gid) + os.chmod(self._config["BUILD_PREFIX"],00775) + + if not os.path.exists(self._config["T"]): + print "creating temp dir" + os.makedirs(self._config["T"]) + os.chown(self._config["T"],portage_uid,portage_gid) + os.chmod(self._config["T"],0770) + + logdir = self._config["T"]+"/logging" + if not os.path.exists(logdir): + os.makedirs(logdir) + os.chown(logdir, portage_uid, portage_gid) + os.chmod(logdir, 0770) + + try: + #XXX: negative restrict + myrestrict = self._config["PORTAGE_RESTRICT"].split() + if ("nouserpriv" not in myrestrict and "userpriv" not in myrestrict): + if ("userpriv" in self._config.features) and (portage_uid and portage_gid): + if (secpass==2): + if os.path.exists(self._config["HOME"]): + # XXX: Potentially bad, but held down by HOME replacement above. + portage_exec.spawn("rm -Rf "+self._config["HOME"]) + if not os.path.exists(self._config["HOME"]): + os.makedirs(self._config["HOME"]) + elif ("userpriv" in features): + print "!!! Disabling userpriv from features... Portage UID/GID not valid." + del features[features.index("userpriv")] + except (IOError, OSError), e: + print "!!! Couldn't empty HOME:",self._config["HOME"] + print "!!!",e + + + try: + # no reason to check for depend since depend returns above. + if not os.path.exists(self._config["BUILD_PREFIX"]): + os.makedirs(self._config["BUILD_PREFIX"]) + os.chown(self._config["BUILD_PREFIX"],portage_uid,portage_gid) + if not os.path.exists(self._config["BUILDDIR"]): + os.makedirs(self._config["BUILDDIR"]) + os.chown(self._config["BUILDDIR"],portage_uid,portage_gid) + + + except OSError, e: + print "!!! File system problem. (ReadOnly? Out of space?)" + print "!!! Perhaps: rm -Rf",self._config["BUILD_PREFIX"] + print "!!!",str(e) + return 1 + + try: + if not os.path.exists(self._config["HOME"]): + os.makedirs(self._config["HOME"]) + os.chown(self._config["HOME"],portage_uid,portage_gid) + os.chmod(self._config["HOME"],02770) + + except OSError, e: + print "!!! File system problem. (ReadOnly? Out of space?)" + print "!!! Failed to create fake home directory in BUILDDIR" + print "!!!",str(e) + return 1 + + try: + if ("userpriv" in features) and ("ccache" in features): + if (not self._config.has_key("CCACHE_DIR")) or (self._config["CCACHE_DIR"]==""): + self._config["CCACHE_DIR"]=self._config["PORTAGE_TMPDIR"]+"/ccache" + if not os.path.exists(self._config["CCACHE_DIR"]): + os.makedirs(self._config["CCACHE_DIR"]) + os.chown(self._config["CCACHE_DIR"],portage_uid,portage_gid) + os.chmod(self._config["CCACHE_DIR"],0775) + except OSError, e: + print "!!! File system problem. (ReadOnly? Out of space?)" + print "!!! Perhaps: rm -Rf",self._config["BUILD_PREFIX"] + print "!!!",str(e) + return 1 + + try: + mystat=os.stat(self._config["CCACHE_DIR"]) + if (mystat[stat.ST_GID]!=portage_gid) or ((mystat[stat.ST_MODE]&02070)!=02070): + print "*** Adjusting ccache permissions for portage user..." + os.chown(self._config["CCACHE_DIR"],portage_uid,portage_gid) + os.chmod(self._config["CCACHE_DIR"],02770) + portage_exec.spawn("chown -R "+str(portage_uid)+":"+str(portage_gid)+" "+self._config["CCACHE_DIR"]) + portage_exec.spawn("chmod -R g+rw "+self._config["CCACHE_DIR"]) + except (OSError, IOError): + pass + + if "distcc" in features: + try: + if (not self._config.has_key("DISTCC_DIR")) or (self._config["DISTCC_DIR"]==""): + self._config["DISTCC_DIR"]=self._config["PORTAGE_TMPDIR"]+"/portage/.distcc" + if not os.path.exists(self._config["DISTCC_DIR"]): + os.makedirs(self._config["DISTCC_DIR"]) + os.chown(self._config["DISTCC_DIR"],portage_uid,portage_gid) + os.chmod(self._config["DISTCC_DIR"],02775) + for x in ("/lock", "/state"): + if not os.path.exists(self._config["DISTCC_DIR"]+x): + os.mkdir(self._config["DISTCC_DIR"]+x) + os.chown(self._config["DISTCC_DIR"]+x,portage_uid,portage_gid) + os.chmod(self._config["DISTCC_DIR"]+x,02775) + except OSError, e: + writemsg("\n!!! File system problem when setting DISTCC_DIR directory permissions.\n") + writemsg( "!!! DISTCC_DIR="+str(self._config["DISTCC_DIR"]+"\n")) + writemsg( "!!! "+str(e)+"\n\n") + time.sleep(5) + features.remove("distcc") + self._config["DISTCC_DIR"]="" + + # break off into process_phase + if self._config.has_key("PORT_LOGDIR"): + try: + st=os.stat(self._config["PORT_LOGDIR"]) + if not st.st_gid == portage_gid: + os.chown(self._config["PORT_LOGDIR"], -1, portage_gid) + if not st.st_mode & (os.W_OK << 3): + os.chmod(self._config["PORT_LOGDIR"], st.st_mode | (os.W_OK << 3)) + # by this time, we have write access to the logdir. or it's bailed. + try: + os.chown(self._config["BUILD_PREFIX"],portage_uid,portage_gid) + os.chmod(self._config["PORT_LOGDIR"],00770) + if not self._config.has_key("LOG_PF") or (self._config["LOG_PF"] != self._config["PF"]): + self._config["LOG_PF"]=self._config["PF"] + self._config["LOG_COUNTER"]=str(db[myroot]["vartree"].dbapi.get_counter_tick_core("/")) + self._config["PORTAGE_LOGFILE"]="%s/%s-%s.log" % (self._config["PORT_LOGDIR"],self._config["LOG_COUNTER"],self._config["LOG_PF"]) + if os.path.exists(self._config["PORTAGE_LOGFILE"]): + os.chmod(self._config["PORTAGE_LOGFILE"], 0664) + os.chown(self._config["PORTAGE_LOGFILE"], -1,portage_gid) + except ValueError, e: + self._config["PORT_LOGDIR"]="" + print "!!! Unable to chown/chmod PORT_LOGDIR. Disabling logging." + print "!!!",e + except (OSError, IOError): + print "!!! Cannot create log... No write access / Does not exist" + print "!!! PORT_LOGDIR:",self._config["PORT_LOGDIR"] + self._config["PORT_LOGDIR"]="" + + # if any of these are being called, handle them -- running them out of the sandbox -- and stop now. + if mydo in ["help","setup"]: + return 0 +# return spawn(EBUILD_SH_BINARY+" "+mydo,self._config,debug=debug,free=1,logfile=logfile) + elif mydo in ["prerm","postrm","preinst","postinst","config"]: + self._config.load_infodir(pkg_dir) + if not use_info_env: + print "overloading port_env_file setting to %s" % self._config["T"]+"/environment" + self._config["PORT_ENV_FILE"] = self._config["T"] + "/environment" + if not os.path.exists(self._config["PORT_ENV_FILE"]): + from output import red + print red("!!!")+" err.. it doesn't exist. that's bad." + sys.exit(1) + return 0 +# return spawn(EBUILD_SH_BINARY+" "+mydo,self._config,debug=debug,free=1,logfile=logfile) + + try: + self._config["SLOT"], self._config["RESTRICT"] = db["/"]["porttree"].dbapi.aux_get(mycpv,["SLOT","RESTRICT"]) + except (IOError,KeyError): + print red("doebuild():")+" aux_get() error reading "+mycpv+"; aborting." + sys.exit(1) + + #initial dep checks complete; time to process main commands + + nosandbox=(("userpriv" in features) and ("usersandbox" not in features)) + actionmap={ + "depend": { "args":(0,1)}, # sandbox / portage + "setup": { "args":(1,0)}, # without / root + "unpack": {"dep":"setup", "args":(0,1)}, # sandbox / portage + "compile": {"dep":"unpack", "args":(nosandbox,1)}, # optional / portage + "test": {"dep":"compile", "args":(nosandbox,1)}, # optional / portage + "install": {"dep":"test", "args":(0,0)}, # sandbox / root + "rpm": {"dep":"install", "args":(0,0)}, # sandbox / root + "package": {"dep":"install", "args":(0,0)}, # sandbox / root + } + + if mydo in actionmap.keys(): + if mydo=="package": + for x in ["","/"+self._config["CATEGORY"],"/All"]: + if not os.path.exists(self._config["PKGDIR"]+x): + os.makedirs(self._config["PKGDIR"]+x) + # REBUILD CODE FOR TBZ2 --- XXXX + return 0 +# return spawnebuild(mydo,actionmap,self._config,debug,logfile=logfile) + elif mydo=="qmerge": + #check to ensure install was run. this *only* pops up when users forget it and are using ebuild + bail=False + if not os.path.exists(self._config["BUILDDIR"]+"/.completed_stages"): + bail=True + else: + myf=open(self._config["BUILDDIR"]+"/.completed_stages") + myd=myf.readlines() + myf.close() + if len(myd) == 0: + bail = True + else: + bail = ("install" not in myd[0].split()) + if bail: + print "!!! mydo=qmerge, but install phase hasn't been ran" + sys.exit(1) + + #qmerge is specifically not supposed to do a runtime dep check + return 0 +# return merge(self._config["CATEGORY"],self._config["PF"],self._config["D"],self._config["BUILDDIR"]+"/build-info",myroot,self._config) + elif mydo=="merge": + return 0 +# retval=spawnebuild("install",actionmap,self._config,debug,alwaysdep=1,logfile=logfile) + if retval: + return retval + +# return merge(self._config["CATEGORY"],self._config["PF"],self._config["D"],self._config["BUILDDIR"]+"/build-info",myroot,self._config,myebuild=self._config["EBUILD"]) + else: + print "!!! Unknown mydo:",mydo + sys.exit(1) + + # phases + # my... god... this... is... ugly. + # we're talking red headed step child of medusa ugly here. + + def process_phase(self,phase,myebuild,myroot,allstages=False,**keywords): + """the public 'doebuild' interface- all phases are called here, along w/ a valid config + allstages is the equivalent of 'do merge, and all needed phases to get to it' + **keywords is options passed on to __adjust_env. It will be removed as __adjust_env is digested""" + from portage import merge,unmerge,features + + validcommands = ["help","clean","prerm","postrm","preinst","postinst", + "config","setup","depend","fetch","digest", + "unpack","compile","test","install","rpm","qmerge","merge", + "package","unmerge", "manifest"] + + if phase not in validcommands: + validcommands.sort() + writemsg("!!! doebuild: '%s' is not one of the following valid commands:" % phase) + for vcount in range(len(validcommands)): + if vcount%6 == 0: + writemsg("\n!!! ") + writemsg(string.ljust(validcommands[vcount], 11)) + writemsg("\n") + return 1 + + retval=self.__adjust_env(phase,myebuild,myroot,**keywords) + if retval: + return retval + + if "userpriv" in features: + sandbox = ("usersandbox" in features) + else: + sandbox = ("sandbox" in features) + + droppriv=(("userpriv" in features) and \ + ("nouserpriv" not in string.split(self._config["PORTAGE_RESTRICT"])) and portage_exec.userpriv_capable) + use_fakeroot=(("userpriv_fakeroot" in features) and droppriv and portage_exec.fakeroot_capable) + + # basically a nasty graph of 'w/ this phase, have it userprived/sandboxed/fakeroot', and run + # these phases prior + actionmap={ + "depend": { "sandbox":False, "userpriv":True, "fakeroot":False}, + "setup": { "sandbox":True, "userpriv":False, "fakeroot":False}, + "unpack": {"dep":"setup", "sandbox":sandbox, "userpriv":True, "fakeroot":False}, + "compile": {"dep":"unpack", "sandbox":sandbox,"userpriv":True, "fakeroot":False}, + "test": {"dep":"compile","sandbox":sandbox,"userpriv":True, "fakeroot":False}, + "install": {"dep":"test", "sandbox":(not use_fakeroot or (not use_fakeroot and sandbox)), + "userpriv":use_fakeroot,"fakeroot":use_fakeroot}, + "rpm": {"dep":"install","sandbox":False, "userpriv":use_fakeroot, "fakeroot":use_fakeroot}, + "package": {"dep":"install", "sandbox":False, "userpriv":use_fakeroot, "fakeroot":use_fakeroot}, + "merge" : {"dep":"install", "sandbox":True, "userpriv":False, "fakeroot":False} + } + + merging=False + # this shouldn't technically ever be called, get_keys exists for this. + # left in for compatability while portage.doebuild still exists + if phase=="depend": + return retval + elif phase=="unmerge": + return unmerge(self._config["CATEGORY"],self._config["PF"],myroot,self._config) + elif phase in ["fetch","digest","manifest","clean"]: + return retval + elif phase=="merge": + merging=True + elif phase=="qmerge": + #no phases ran. + phase="merge" + merging=True +# return merge(self._config["CATEGORY"],self._config["PF"],self._config["D"],self._config["BUILDDIR"]+"/build-info",myroot,\ +# self._config) + + elif phase in ["help","clean","prerm","postrm","preinst","postinst","config"]: + self.__ebp = request_ebuild_processor(self._config, userpriv=False) + self.__ebp.write("process_ebuild %s" % phase) + self.__ebp.send_env(self._config) + self.__ebp.set_sandbox_state(phase in ["help","clean"]) + self.__ebp.write("start_processing") + retval = self.__generic_phase([],self._config) + release_ebuild_processor(self.__ebp) + self.__ebp = None + return not retval + + k=phase + # represent the phases to run, grouping each phase based upon if it's sandboxed, fakerooted, and userpriv'd + # ugly at a glance, but remember a processor can run multiple phases now. + # best to not be wasteful in terms of env saving/restoring, and just run all applicable phases in one shot + phases=[[[phase]]] + sandboxed=[[actionmap[phase]["sandbox"]]] + privs=[(actionmap[phase]["userpriv"],actionmap[phase]["fakeroot"])] + + if allstages: + while actionmap[k].has_key("dep"): + k=actionmap[k]["dep"] + if actionmap[k]["userpriv"] != privs[-1][0] or actionmap[k]["fakeroot"] != privs[-1][1]: + phases.append([[k]]) + sandboxed.append([actionmap[k]["sandbox"]]) + privs.append((actionmap[k]["userpriv"],actionmap[k]["fakeroot"])) + elif actionmap[k]["sandbox"] != sandboxed[-1][-1]: + phases[-1].append([k]) + sandboxed[-1].extend([actionmap[k]["sandbox"]]) + else: + phases[-1][-1].append(k) + privs.reverse() + phases.reverse() + sandboxed.reverse() + for x in phases: + for y in x: + y.reverse() + x.reverse() + # and now we have our phases grouped in parallel to the sandbox/userpriv/fakeroot state. + + all_phases = portage_util.flatten(phases) + +# print "all_phases=",all_phases +# print "phases=",phases +# print "sandbox=",sandboxed +# print "privs=",privs +# sys.exit(1) +# print "\n\ndroppriv=",droppriv,"use_fakeroot=",use_fakeroot,"\n\n" + + #temporary hack until sandbox + fakeroot (if ever) play nice. + while privs: + if self.__ebp == None or (droppriv and self.__ebp.userprived() != privs[0][0]) or \ + (use_fakeroot and self.__ebp.fakerooted() != privs[0][1]): + if self.__ebp != None: + print "swapping processors for",phases[0][0] + release_ebuild_processor(self.__ebp) + self.__ebp = None + opts={} + + #only engage fakeroot when userpriv'd + if use_fakeroot and privs[0][1]: + opts["save_file"] = self._config["T"]+"/fakeroot_db" + + self.__ebp = request_ebuild_processor(self._config, userpriv=(privs[0][0] and droppriv), \ + fakeroot=(privs[0][1] and use_fakeroot), \ + + sandbox=(not (privs[0][1] and use_fakeroot) and portage_exec.sandbox_capable),**opts) + + #loop through the instances where the processor must have the same sandboxed state- + #note a sandbox'd process can have it's sandbox disabled. + #this seperation is needed since you can't mix sandbox and fakeroot atm. + for sandbox in sandboxed[0]: + if "merge" in phases[0][0]: + if len(phases[0][0]) == 1: + print "skipping this phase, it's just merge" + continue + phases[0][0].remove("merge") + + self.__ebp.write("process_ebuild %s" % string.join(phases[0][0]," ")) + self.__ebp.send_env(self._config) + self.__ebp.set_sandbox_state(sandbox) + self.__ebp.write("start_processing") + phases[0].pop(0) + retval = not self.__generic_phase([],self._config) + if retval: + release_ebuild_processor(self.__ebp) + self.__ebp = None + return retval + sandboxed.pop(0) + privs.pop(0) + phases.pop(0) + # hey hey. we're done. Now give it back. + release_ebuild_processor(self.__ebp) + self.__ebp = None + + # packaging moved out of ebuild.sh, and into this code. + # makes it so ebuild.sh no longer must run as root for the package phase. + if "package" in all_phases: + print "processing package" + #mv "${PF}.tbz2" "${PKGDIR}/All" + if not os.path.exists(self._config["PKGDIR"]+"/All"): + os.makedirs(self._config["PKGDIR"]+"/All") + if not os.path.exists(self._config["PKGDIR"]+"/"+self._config["CATEGORY"]): + os.makedirs(self._config["PKGDIR"]+"/"+self._config["CATEGORY"]) + if os.path.exists("%s/All/%s.tbz2" % (self._config["PKGDIR"],self._config["PF"])): + os.remove("%s/All/%s.tbz2" % (self._config["PKGDIR"],self._config["PF"])) + retval = not portage_util.movefile("%s/%s.tbz2" % (self._config["BUILDDIR"],self._config["PF"]), + self._config["PKGDIR"]+"/All/"+self._config["PF"]+".tbz2") > 0 + if retval: return False + if os.path.exists("%s/%s/%s.tbz2" % (self._config["PKGDIR"],self._config["CATEGORY"],self._config["PF"])): + os.remove("%s/%s/%s.tbz2" % (self._config["PKGDIR"],self._config["CATEGORY"],self._config["PF"])) + os.symlink("%s/All/%s.tbz2" % (self._config["PKGDIR"],self._config["PF"]), + "%s/%s/%s.tbz2" % (self._config["PKGDIR"],self._config["CATEGORY"],self._config["PF"])) + + #same as the package phase above, removes the root requirement for the rpm phase. + if "rpm" in all_phases: + rpm_name="%s-%s-%s" % (self._config["PN"],self._config["PV"],self._config["PR"]) + + retval = not portage_util.movefile("%s/%s.tar.gz" % (self._config["T"],self._config["PF"]), + "/usr/src/redhat/SOURCES/%s.tar.gz" % self._config["PF"]) > 0 + if retval: + print "moving src for rpm failed, retval=",retval + return False + + retval=portage_exec.spawn(("rpmbuild","-bb","%s/%s.spec" % \ + (self._config["BUILDDIR"],self._config["PF"]))) + if retval: + print "Failed to integrate rpm spec file" + return retval + + if not os.path.exists(self._config["RPMDIR"]+"/"+self._config["CATEGORY"]): + os.makedirs(self._config["RPMDIR"]+"/"+self._config["CATEGORY"]) + + retval = not portage_util.movefile("/usr/src/redhat/RPMS/i386/%s.i386.rpm" % rpm_name, + "%s/%s/%s.rpm" % (self._config["RPMDIR"],self._config["CATEGORY"],rpm_name)) > 0 + if retval: + print "rpm failed" + return retval + + + # not great check, but it works. + # basically, if FEATURES="-buildpkg" emerge package was called, the files in the current + # image directory don't have their actual perms. so we use an ugly bit of bash + # to make the fakeroot (claimed) permissions/owners a reality. + if use_fakeroot and os.path.exists(self._config["T"]+"/fakeroot_db") and merging: + print "correcting fakeroot privs" + retval=portage_exec.spawn(("/usr/lib/portage/bin/affect-fakeroot-perms.sh", \ + self._config["T"]+"/fakeroot_db", \ + self._config["D"]),env={"BASHRC":portage_const.INVALID_ENV_FILE}) + if retval or retval == None: + print red("!!!")+"affecting fakeroot perms after the fact failed" + return retval + + if merging: + print "processing merge" + retval = merge(self._config["CATEGORY"],self._config["PF"],self._config["D"],self._config["BUILDDIR"]+"/build-info",myroot,\ + self._config,myebuild=self._config["EBUILD"]) + return retval + + # this basically handles all hijacks from the daemon, whether confcache or portageq. + def __generic_phase(self,breakers,interpret_results=True): + """internal function that responds to the running ebuild processor's requests + this enables portageq hijack, sandbox summaries, confcache among other things + interpret_results controls whether this returns true/false, or the string the + processor spoke that caused this to release control + breaks is list of strings that cause this loop/interpretter to relinquish control""" + b = breakers[:] + b.extend(["prob","phases failed","phases succeeded","env_receiving_failed"]) + line='' + while line not in b: + line=self.__ebp.read() + line=line[:-1] + + if line[0:23] == "request_sandbox_summary": + self.__ebp.sandbox_summary(line[24:]) + elif line[0:17] == "request_confcache": + self.load_confcache(line[18:]) + elif line[0:16] == "update_confcache": + k=line[17:].split() + # sandbox_debug_log, local_cache + self.update_confcache(self._config,k[0],k[1]) + elif line[0:8] == "portageq": + keys=line[8:].split() + try: + e,s=getattr(self.portageq,keys[0])(keys[1:]) + except SystemExit, e: + raise + except Exception, ex: + sys.stderr.write("caught exception %s\n" % str(ex)) + e=2 + s="ERROR: insufficient paramters!" + self.__ebp.write("return_code="+str(e)) + if len(s): + self.__ebp.write(s) + self.__ebp.write("stop_text") + self.processed += 1 + if interpret_results: + return (line=="phases succeeded") + return line + |