2 import sys,string,os,types,re,signal,traceback,time
4 selinux_capable = False
5 #userpriv_capable = (os.getuid() == 0)
6 #fakeroot_capable = False
7 BASH_BINARY = "/bin/bash"
11 max_fd_limit=resource.getrlimit(RLIMIT_NOFILE)
15 # hokay, no resource module.
18 # pids this process knows of.
26 def cleanup(pids,block_exceptions=True):
27 """function to go through and reap the list of pids passed to it"""
33 os.kill(x,signal.SIGTERM)
34 if os.waitpid(x,os.WNOHANG)[1] == 0:
35 # feisty bugger, still alive.
36 os.kill(x,signal.SIGKILL)
42 if oe.errno not in (10,3):
49 try: spawned_pids.remove(x)
50 except IndexError: pass
54 # a function to turn a string of non-printable characters into a string of
57 hexStr = string.hexdigits
61 r = r + hexStr[(i >> 4) & 0xF] + hexStr[i & 0xF]
66 def read_from_clst(file):
73 #raise CatalystError, "Could not open file "+file
74 for line in myf.readlines():
75 #line = string.replace(line, "\n", "") # drop newline
76 myline = myline + line
81 # these should never be touched
82 required_build_targets=["generic_target","generic_stage_target"]
84 # new build types should be added here
85 valid_build_targets=["stage1_target","stage2_target","stage3_target","stage4_target","grp_target",
86 "livecd_stage1_target","livecd_stage2_target","embedded_target",
87 "tinderbox_target","snapshot_target","netboot_target","netboot2_target"]
89 required_config_file_values=["storedir","sharedir","distdir","portdir"]
90 valid_config_file_values=required_config_file_values[:]
91 valid_config_file_values.append("PKGCACHE")
92 valid_config_file_values.append("KERNCACHE")
93 valid_config_file_values.append("CCACHE")
94 valid_config_file_values.append("DISTCC")
95 valid_config_file_values.append("ICECREAM")
96 valid_config_file_values.append("ENVSCRIPT")
97 valid_config_file_values.append("AUTORESUME")
98 valid_config_file_values.append("FETCH")
99 valid_config_file_values.append("CLEAR_AUTORESUME")
100 valid_config_file_values.append("options")
101 valid_config_file_values.append("DEBUG")
102 valid_config_file_values.append("VERBOSE")
103 valid_config_file_values.append("PURGE")
104 valid_config_file_values.append("PURGEONLY")
105 valid_config_file_values.append("SNAPCACHE")
106 valid_config_file_values.append("snapshot_cache")
107 valid_config_file_values.append("hash_function")
108 valid_config_file_values.append("digests")
109 valid_config_file_values.append("contents")
110 valid_config_file_values.append("SEEDCACHE")
114 def list_bashify(mylist):
115 if type(mylist)==types.StringType:
119 for x in range(0,len(mypack)):
120 # surround args with quotes for passing to bash,
121 # allows things like "<" to remain intact
122 mypack[x]="'"+mypack[x]+"'"
123 mypack=string.join(mypack)
126 def list_to_string(mylist):
127 if type(mylist)==types.StringType:
131 for x in range(0,len(mypack)):
132 # surround args with quotes for passing to bash,
133 # allows things like "<" to remain intact
135 mypack=string.join(mypack)
138 class CatalystError(Exception):
139 def __init__(self, message):
141 (type,value)=sys.exc_info()[:2]
144 print traceback.print_exc(file=sys.stdout)
146 print "!!! catalyst: "+message
149 class LockInUse(Exception):
150 def __init__(self, message):
152 #(type,value)=sys.exc_info()[:2]
155 #kprint traceback.print_exc(file=sys.stdout)
157 print "!!! catalyst lock file in use: "+message
165 print "!!! catalyst: "+msg
167 def find_binary(myc):
168 """look through the environmental path for an executable file named whatever myc is"""
173 for x in p.split(":"):
174 #if it exists, and is executable
175 if os.path.exists("%s/%s" % (x,myc)) and os.stat("%s/%s" % (x,myc))[0] & 0x0248:
176 return "%s/%s" % (x,myc)
179 def spawn_bash(mycommand,env={},debug=False,opt_name=None,**keywords):
180 """spawn mycommand as an arguement to bash"""
183 opt_name=mycommand.split()[0]
184 if "BASH_ENV" not in env:
185 env["BASH_ENV"] = "/etc/spork/is/not/valid/profile.env"
189 args.append(mycommand)
190 return spawn(args,env=env,opt_name=opt_name,**keywords)
192 #def spawn_get_output(mycommand,spawn_type=spawn,raw_exit_code=False,emulate_gso=True, \
193 # collect_fds=[1],fd_pipes=None,**keywords):
195 def spawn_get_output(mycommand,raw_exit_code=False,emulate_gso=True, \
196 collect_fds=[1],fd_pipes=None,**keywords):
197 """call spawn, collecting the output to fd's specified in collect_fds list
198 emulate_gso is a compatability hack to emulate commands.getstatusoutput's return, minus the
199 requirement it always be a bash call (spawn_type controls the actual spawn call), and minus the
200 'lets let log only stdin and let stderr slide by'.
202 emulate_gso was deprecated from the day it was added, so convert your code over.
203 spawn_type is the passed in function to call- typically spawn_bash, spawn, spawn_sandbox, or spawn_fakeroot"""
204 global selinux_capable
207 #if type(spawn_type) not in [types.FunctionType, types.MethodType]:
208 # s="spawn_type must be passed a function, not",type(spawn_type),spawn_type
215 for x in collect_fds:
217 keywords["returnpid"]=True
219 mypid=spawn_bash(mycommand,fd_pipes=fd_pipes,**keywords)
221 if type(mypid) != types.ListType:
223 return [mypid, "%s: No such file or directory" % mycommand.split()[0]]
226 mydata=fd.readlines()
229 mydata=string.join(mydata)
230 if len(mydata) and mydata[-1] == "\n":
232 retval=os.waitpid(mypid[0],0)[1]
235 return [retval,mydata]
236 retval=process_exit_code(retval)
237 return [retval, mydata]
239 # base spawn function
240 def spawn(mycommand,env={},raw_exit_code=False,opt_name=None,fd_pipes=None,returnpid=False,\
241 uid=None,gid=None,groups=None,umask=None,logfile=None,path_lookup=True,\
242 selinux_context=None, raise_signals=False, func_call=False):
243 """base fork/execve function.
244 mycommand is the desired command- if you need a command to execute in a bash/sandbox/fakeroot
245 environment, use the appropriate spawn call. This is a straight fork/exec code path.
246 Can either have a tuple, or a string passed in. If uid/gid/groups/umask specified, it changes
247 the forked process to said value. If path_lookup is on, a non-absolute command will be converted
248 to an absolute command, otherwise it returns None.
250 selinux_context is the desired context, dependant on selinux being available.
251 opt_name controls the name the processor goes by.
252 fd_pipes controls which file descriptor numbers are left open in the forked process- it's a dict of
253 current fd's raw fd #, desired #.
255 func_call is a boolean for specifying to execute a python function- use spawn_func instead.
256 raise_signals is questionable. Basically throw an exception if signal'd. No exception is thrown
259 logfile overloads the specified fd's to write to a tee process which logs to logfile
260 returnpid returns the relevant pids (a list, including the logging process if logfile is on).
262 non-returnpid calls to spawn will block till the process has exited, returning the exitcode/signal
263 raw_exit_code controls whether the actual waitpid result is returned, or intrepretted."""
267 if type(mycommand)==types.StringType:
268 mycommand=mycommand.split()
270 if not os.access(myc, os.X_OK):
273 myc = find_binary(myc)
279 mypid.extend(spawn(('tee','-i','-a',logfile),returnpid=True,fd_pipes={0:pr,1:1,2:2}))
280 retval=os.waitpid(mypid[-1],os.WNOHANG)[1]
285 return process_exit_code(retval)
294 opt_name = mycommand[0]
296 myargs.extend(mycommand[1:])
298 mypid.append(os.fork())
301 spawned_pids.extend(mypid)
307 # this may look ugly, but basically it moves file descriptors around to ensure no
308 # handles that are needed are accidentally closed during the final dup2 calls.
310 if type(fd_pipes)==types.DictType:
315 #build list of which fds will be where, and where they are at currently
318 src_fd.append(fd_pipes[x])
320 # run through said list dup'ing descriptors so that they won't be waxed
321 # by other dup calls.
322 for x in range(0,len(trg_fd)):
323 if trg_fd[x] == src_fd[x]:
325 if trg_fd[x] in src_fd[x+1:]:
326 new=os.dup2(trg_fd[x],max(src_fd) + 1)
330 src_fd[s.index(trg_fd[x])]=new
331 except SystemExit, e:
336 # transfer the fds to their final pre-exec position.
337 for x in range(0,len(trg_fd)):
338 if trg_fd[x] != src_fd[x]:
339 os.dup2(src_fd[x], trg_fd[x])
343 # wax all open descriptors that weren't requested be left open.
344 for x in range(0,max_fd_limit):
348 except SystemExit, e:
353 # note this order must be preserved- can't change gid/groups if you change uid first.
354 if selinux_capable and selinux_context:
356 selinux.setexec(selinux_context)
369 #print "execing", myc, myargs
371 # either use a passed in func for interpretting the results, or return if no exception.
372 # note the passed in list, and dict are expanded.
373 if len(mycommand) == 4:
374 os._exit(mycommand[3](mycommand[0](*mycommand[1],**mycommand[2])))
376 mycommand[0](*mycommand[1],**mycommand[2])
378 print "caught exception",e," in forked func",mycommand[0]
381 #os.execvp(myc,myargs)
382 os.execve(myc,myargs,env)
383 except SystemExit, e:
387 raise str(e)+":\n "+myc+" "+string.join(myargs)
388 print "func call failed"
390 # If the execve fails, we need to report it, and exit
391 # *carefully* --- report error here
394 return # should never get reached
396 # if we were logging, kill the pipes.
404 # loop through pids (typically one, unless logging), either waiting on their death, or waxing them
405 # if the main pid (mycommand) returned badly.
407 retval=os.waitpid(mypid[-1],0)[1]
409 cleanup(mypid[0:-1],block_exceptions=False)
410 # at this point we've killed all other kid pids generated via this call.
414 return process_exit_code(retval,throw_signals=raise_signals)
420 def cmd(mycmd,myexc="",env={}):
423 retval=spawn_bash(mycmd,env)
425 raise CatalystError,myexc
429 def process_exit_code(retval,throw_signals=False):
430 """process a waitpid returned exit code, returning exit code if it exit'd, or the
431 signal if it died from signalling
432 if throw_signals is on, it raises a SystemExit if the process was signaled.
433 This is intended for usage with threads, although at the moment you can't signal individual
434 threads in python, only the master thread, so it's a questionable option."""
435 if (retval & 0xff)==0:
436 return retval >> 8 # return exit code
439 #use systemexit, since portage is stupid about exception catching.
441 return (retval & 0xff) << 8 # interrupted by signal
443 def file_locate(settings,filelist,expand=1):
444 #if expand=1, non-absolute paths will be accepted and
445 # expanded to os.getcwd()+"/"+localpath if file exists
446 for myfile in filelist:
447 if myfile not in settings:
448 #filenames such as cdtar are optional, so we don't assume the variable is defined.
451 if len(settings[myfile])==0:
452 raise CatalystError, "File variable \""+myfile+"\" has a length of zero (not specified.)"
453 if settings[myfile][0]=="/":
454 if not os.path.exists(settings[myfile]):
455 raise CatalystError, "Cannot locate specified "+myfile+": "+settings[myfile]
456 elif expand and os.path.exists(os.getcwd()+"/"+settings[myfile]):
457 settings[myfile]=os.getcwd()+"/"+settings[myfile]
459 raise CatalystError, "Cannot locate specified "+myfile+": "+settings[myfile]+" (2nd try)"
463 The spec file format is a very simple and easy-to-use format for storing data. Here's an example
473 This file would be interpreted as defining three items: item1, item2 and item3. item1 would contain
474 the string value "value1". Item2 would contain an ordered list [ "foo", "bar", "oni" ]. item3
475 would contain an ordered list as well: [ "meep", "bark", "gleep", "moop" ]. It's important to note
476 that the order of multiple-value items is preserved, but the order that the items themselves are
477 defined are not preserved. In other words, "foo", "bar", "oni" ordering is preserved but "item1"
478 "item2" "item3" ordering is not, as the item strings are stored in a dictionary (hash).
481 def parse_makeconf(mylines):
484 pat=re.compile("([0-9a-zA-Z_]*)=(.*)")
485 while pos<len(mylines):
486 if len(mylines[pos])<=1:
490 if mylines[pos][0] in ["#"," ","\t"]:
491 #skip indented lines, comments
496 mobj=pat.match(myline)
499 clean_string = re.sub(r"\"",r"",mobj.group(2))
500 mymakeconf[mobj.group(1)]=clean_string
503 def read_makeconf(mymakeconffile):
504 if os.path.exists(mymakeconffile):
507 import snakeoil.bash #import snakeoil.fileutils
508 return snakeoil.bash.read_bash_dict(mymakeconffile, sourcing_command="source")
512 return portage.util.getconfig(mymakeconffile, tolerant=1, allow_sourcing=True)
516 return portage_util.getconfig(mymakeconffile, tolerant=1, allow_sourcing=True)
518 myf=open(mymakeconffile,"r")
519 mylines=myf.readlines()
521 return parse_makeconf(mylines)
523 raise CatalystError, "Could not parse make.conf file "+mymakeconffile
528 def msg(mymsg,verblevel=1):
529 if verbosity>=verblevel:
532 def pathcompare(path1,path2):
533 # Change double slashes to slash
534 path1 = re.sub(r"//",r"/",path1)
535 path2 = re.sub(r"//",r"/",path2)
536 # Removing ending slash
537 path1 = re.sub("/$","",path1)
538 path2 = re.sub("/$","",path2)
545 "enhanced to handle bind mounts"
546 if os.path.ismount(path):
549 mylines=a.readlines()
553 if pathcompare(path,mysplit[2]):
557 def addl_arg_parse(myspec,addlargs,requiredspec,validspec):
558 "helper function to help targets parse additional arguments"
559 global valid_config_file_values
562 for x in addlargs.keys():
563 if x not in validspec and x not in valid_config_file_values and x not in requiredspec:
564 messages.append("Argument \""+x+"\" not recognized.")
566 myspec[x]=addlargs[x]
568 for x in requiredspec:
570 messages.append("Required argument \""+x+"\" not specified.")
573 raise CatalystError, '\n\tAlso: '.join(messages)
580 raise CatalystError, "Could not touch "+myfile+"."
582 def countdown(secs=5, doing="Starting"):
584 print ">>> Waiting",secs,"seconds before starting..."
585 print ">>> (Control-C to abort)...\n"+doing+" in: ",
589 sys.stdout.write(str(sec+1)+" ")
594 def normpath(mypath):
596 if mypath[-1] == "/":
598 newpath = os.path.normpath(mypath)
600 if newpath[:2] == "//":
601 newpath = newpath[1:]