mirror of
https://gitdl.cn/https://github.com/chakralinux/core.git
synced 2025-02-05 15:37:14 +08:00
673 lines
20 KiB
Python
Executable File
673 lines
20 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# larch.py
|
|
#
|
|
# (c) Copyright 2009-2010 Michael Towers (larch42 at googlemail dot com)
|
|
#
|
|
# This file is part of the larch project.
|
|
#
|
|
# larch is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# larch is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with larch; if not, write to the Free Software Foundation, Inc.,
|
|
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
#
|
|
#----------------------------------------------------------------------------
|
|
# 2010.02.12
|
|
|
|
|
|
"""
|
|
This is the main module of the larch program. It needs to run most of the
|
|
larch-building commands as root, but it is generally better to run it as
|
|
a non-privileged user - it can ask for the root password when it needs it.
|
|
One advantage of running as a normal user is that all the larch
|
|
configuration files are stored by default in the user's home directory,
|
|
and it would be inconvenient to have these only accessible as root.
|
|
|
|
The graphical user interface runs as a separate process and the
|
|
communication is via pipes to the subprocess's stdio channels.
|
|
Data is passed as json objects.
|
|
|
|
The command-line user interface works in a very different way, though it
|
|
still uses signals to some extent to control execution. It does not start
|
|
a separate process, it just handles the commands passed on the command line
|
|
sequentially.
|
|
|
|
The main work of the program (the steering of the live system
|
|
construction) is handled by the imported modules and run in a separate
|
|
thread.
|
|
"""
|
|
|
|
import os, sys, traceback, re, pwd, signal, time
|
|
|
|
import __builtin__
|
|
__builtin__.base_dir = os.path.dirname(os.path.realpath(__file__))
|
|
sys.path.append("%s/modules" % base_dir)
|
|
script_dir = "%s/buildscripts" % base_dir
|
|
|
|
from subprocess import Popen, call, PIPE, STDOUT
|
|
import threading, traceback
|
|
from Queue import Queue
|
|
import pexpect
|
|
|
|
import base
|
|
from installation import Installation
|
|
|
|
from projectpage import ProjectPage
|
|
from installpage import InstallPage
|
|
from buildpage import BuildPage
|
|
from mediumpage import MediumPage
|
|
from tweakpage import TweakPage
|
|
|
|
doc_home = [
|
|
"gui_project_settings.html",
|
|
"gui_installation.html",
|
|
"gui_larchify.html",
|
|
"gui_medium.html",
|
|
"gui_tweaks.html"
|
|
]
|
|
|
|
|
|
def errout(text, quitc=0):
|
|
sys.stderr.write(text)
|
|
sys.stderr.flush()
|
|
if quitc:
|
|
exit(quitc)
|
|
__builtin__.errout = errout
|
|
|
|
def debug(text):
|
|
errout("DEBUG: " + text.strip() + "\n")
|
|
__builtin__.debug = debug
|
|
|
|
|
|
import gettext
|
|
gettext.install('larch', base_dir+'/i18n', unicode=1)
|
|
__builtin__.lang = (os.environ.get("LANGUAGE") or os.environ.get("LC_ALL")
|
|
or os.environ.get("LC_MESSAGES") or os.environ.get("LANG"))
|
|
# Some of the subprocesses must be run without i18n because the text
|
|
# output is parsed.
|
|
os.environ["LANGUAGE"] = "C"
|
|
|
|
class Command:
|
|
def __init__(self):
|
|
self.password = None
|
|
# Supershell/worker variables
|
|
self.supershell_process = None
|
|
self.worker_lock = threading.Lock()
|
|
self.worker_breakin = False
|
|
self.worker_active = False
|
|
|
|
# Keep a record of mounts
|
|
self.mounts = []
|
|
|
|
|
|
def gui_init(self):
|
|
"""The user interface must already be running before entering here.
|
|
"""
|
|
ui.init()
|
|
# Initialize gui modules
|
|
self.pages = [ProjectPage(), InstallPage(), BuildPage(),
|
|
MediumPage(), TweakPage()]
|
|
|
|
# Connect up the signals and slots
|
|
self.addconnections([
|
|
("$$$uiquit$$$", self.uiquit),
|
|
("$$$cancel$$$", self.cancel),
|
|
(":showlog*clicked", self._showlog),
|
|
(":docs*clicked", self._showdocs),
|
|
(":notebook*changed", self.pageswitch),
|
|
])
|
|
for p in self.pages:
|
|
self.addconnections(p.connect())
|
|
|
|
|
|
def addconnections(self, connlist):
|
|
for s, f in connlist:
|
|
ui.addslot(s, f)
|
|
|
|
|
|
def run(self):
|
|
# Start on the project page
|
|
self.pageswitch(0)
|
|
ui.go()
|
|
|
|
|
|
def pageswitch(self, index):
|
|
if ui.docviewer:
|
|
ui.docviewer.gohome(doc_home[index])
|
|
if (not self.pages[index].setup()) and (index != 0):
|
|
ui.command(":notebook.set", 0)
|
|
|
|
|
|
def log(self, line):
|
|
# The line should not have a newline terminator
|
|
logqueue.put(line)
|
|
|
|
|
|
def worker(self, fun, *args):
|
|
"""Functions run via this function will have their output displayed
|
|
in the progress view, and it should be possible to cancel them
|
|
manually.
|
|
This method should only be called from a background thread (i.e.
|
|
from an '&'-signal handler.
|
|
"""
|
|
self.worker_lock.acquire()
|
|
if self.worker_active:
|
|
self.worker_lock.release()
|
|
fatal_error("*BUG* Attempted to start second worker thread")
|
|
return
|
|
self.worker_breakin = False
|
|
self.worker_active = True
|
|
self.worker_lock.release()
|
|
|
|
ui.progress.start()
|
|
try:
|
|
fun(*args)
|
|
result = True
|
|
except:
|
|
result = False
|
|
#debug("WORKER: " + traceback.format_exc())
|
|
ui.progress.end()
|
|
|
|
self.worker_lock.acquire()
|
|
self.worker_active = False
|
|
self.worker_lock.release()
|
|
return result
|
|
|
|
|
|
def worker_wait(self):
|
|
"""Used by the cli (only) to wait for the completion of an operation.
|
|
It doesn't use any locking, so it should be used carefully
|
|
"""
|
|
while self.worker_active:
|
|
time.sleep(0.1)
|
|
|
|
|
|
def supershell(self, cmd):
|
|
"""Run a command as root and wait until it completes.
|
|
To be used (only) in the 'worker' thread.
|
|
Return a pair (ok, output)
|
|
Note that 'cmd' may not contain '"' characters!
|
|
To handle 'Cancel' operations, interrupts are checked before and
|
|
after execution of supershell commands.
|
|
"""
|
|
if (os.getuid() != 0) and not self.password:
|
|
while True:
|
|
okpw = ui.textLineDialog(_("Please enter root password"),
|
|
"larch: root pw", pw=True)
|
|
if okpw[0]:
|
|
self.password = okpw[1]
|
|
# Run a command as root, using the known password.
|
|
child = pexpect.spawn('su -c "echo _OK_"')
|
|
child.expect('Password:')
|
|
child.sendline(self.password)
|
|
child.expect(pexpect.EOF)
|
|
o = child.before.strip()
|
|
if o.endswith('_OK_'):
|
|
break
|
|
run_error(_("Incorrect root password"))
|
|
continue
|
|
else:
|
|
run_error( _("No root password, cancelling operation"))
|
|
assert False
|
|
|
|
self.worker_lock.acquire()
|
|
if self.worker_breakin:
|
|
self.worker_lock.release()
|
|
assert False
|
|
|
|
if self.supershell_process:
|
|
self.worker_lock.release()
|
|
fatal_error("*BUG* Attempted to start second supershell process")
|
|
assert False
|
|
|
|
self.supershell_process = self.asroot(cmd)
|
|
if not self.supershell_process:
|
|
self.worker_lock.release()
|
|
fatal_error(_("Supershell couldn't be started"))
|
|
assert False
|
|
|
|
self.log(">" + cmd)
|
|
self.worker_lock.release()
|
|
|
|
result = []
|
|
line0 = ""
|
|
while True:
|
|
ch = self.supershell_process.read(1)
|
|
if not ch:
|
|
if not line0:
|
|
break
|
|
elif ch == "\n":
|
|
continue
|
|
elif ch != "\r":
|
|
line0 += ch
|
|
continue
|
|
line = line0.strip()
|
|
line0 = ""
|
|
self.log(":-" + line)
|
|
result.append(line)
|
|
|
|
self.supershell_process.close()
|
|
rc = self.supershell_process.exitstatus
|
|
|
|
self.worker_lock.acquire()
|
|
self.supershell_process = None
|
|
interrupted = self.worker_breakin or (rc == None)
|
|
self.log("@%d" % (-1 if rc == None else rc))
|
|
self.worker_lock.release()
|
|
|
|
assert not interrupted
|
|
return (rc == 0, result)
|
|
|
|
|
|
def asroot(self, cmd):
|
|
"""Run the command as root with pexpect. Return the spawned process.
|
|
If it cannot be started return None.
|
|
"""
|
|
p = pexpect.spawn('''su -s /bin/bash -c "echo _GO_ && %s"''' % cmd,
|
|
timeout=None)
|
|
e = p.expect(["_GO_.*\n", pexpect.TIMEOUT, "Password:"], 5)
|
|
while e != 0:
|
|
if e == 2:
|
|
p.sendline(self.password)
|
|
e = p.expect(["_GO_.*\n", pexpect.TIMEOUT], 5)
|
|
else:
|
|
return None
|
|
return p
|
|
|
|
|
|
def script(self, cmd):
|
|
s = self.supershell("%s/%s" % (script_dir, cmd))
|
|
if s[0]:
|
|
return ""
|
|
else:
|
|
return "SCRIPT ERROR: (%s)\n" % cmd + "".join(s[1])
|
|
|
|
|
|
def worker_cancel(self):
|
|
"""Cancel the current worker thread.
|
|
This is achieved by inserting traps in the supershell calls.
|
|
"""
|
|
self.worker_lock.acquire()
|
|
if self.supershell_process:
|
|
self.asroot("pkill -g %d" % self.supershell_process.pid)
|
|
self.worker_breakin = True
|
|
self.worker_lock.release()
|
|
|
|
|
|
def _showlog(self):
|
|
ui.runningtab(2)
|
|
|
|
|
|
def _showdocs(self):
|
|
ui.runningtab(3)
|
|
|
|
|
|
def _browse(self, btype, path):
|
|
simple_thread(self._browse_set, btype, path)
|
|
|
|
|
|
def _browse_set(self, btype, path):
|
|
appcall = config.get(btype)
|
|
while (call(["which", appcall.split()[0]],
|
|
stdout=PIPE, stderr=STDOUT) != 0):
|
|
ok, new = ui.textLineDialog(
|
|
_("Enter '%s' application ('$' for path argument):") % btype,
|
|
text=appcall)
|
|
if ok:
|
|
appcall = new
|
|
config.set(btype, appcall)
|
|
else:
|
|
return
|
|
|
|
Popen(appcall.replace("$", path) + " &", shell=True)
|
|
|
|
|
|
def sigint(self, num, frame):
|
|
"""CONSOLE MODE ONLY: A handler for SIGINT. Tidy up properly and quit.
|
|
First kill potential running supershell process, then terminate
|
|
logger, and then, finally, terminate the ui process.
|
|
"""
|
|
self.uiquit()
|
|
|
|
|
|
def uiquit(self):
|
|
self.cancel(True)
|
|
|
|
|
|
def cancel(self, terminate=False):
|
|
# This is a bit experimental - I'm not sure the worker threads will handle
|
|
# the break-ins sensibly.
|
|
# This is not called from the worker thread, so it mustn't block.
|
|
self.qthread = simple_thread(self._quit_run, terminate)
|
|
|
|
|
|
def _quit_run(self, terminate):
|
|
# Kill any running supershell process
|
|
self.worker_cancel()
|
|
self.worker_wait()
|
|
|
|
if terminate:
|
|
# Tell the logger to quit
|
|
self.log("L:/\n")
|
|
# Wait until logger process has terminated
|
|
lthread.join()
|
|
# Tell the user interface to exit
|
|
ui.quit()
|
|
|
|
self.worker_breakin = False
|
|
command.unmount()
|
|
|
|
|
|
def mount(self, src, dst, opts=""):
|
|
if supershell("mount %s %s %s" % (opts, src, dst))[0]:
|
|
self.mounts.append(dst)
|
|
return True
|
|
return False
|
|
|
|
|
|
def unmount(self, dst=None):
|
|
if dst == None:
|
|
mounts = list(self.mounts)
|
|
elif type(dst) in (list, tuple):
|
|
mounts = list(dst)
|
|
else:
|
|
mounts = [dst]
|
|
|
|
r = True
|
|
for m in mounts:
|
|
if supershell("umount %s" % m)[0]:
|
|
self.mounts.remove(m)
|
|
else:
|
|
r = False
|
|
return r
|
|
|
|
|
|
def edit(self, fname, source=None, label=None, filter=None):
|
|
"""The file to be edited can be either an absolute path or else
|
|
relative to the profile directory. If the file already exists its
|
|
contents will be taken as the starting point, otherwise file at
|
|
the 'source' path will be read in. Whichever file is available
|
|
its contents can be filtered by an optional 'filter' function,
|
|
which takes the file contents as a string as argument and
|
|
returns the transformed contents as another string.
|
|
"""
|
|
def revert():
|
|
"""If a file is addressed by 'source' revert to its contents,
|
|
if source is "", clear the contents, otherwise revert to the
|
|
contents as they were before entering the editor.
|
|
"""
|
|
return textsrc if source != None else text0
|
|
|
|
def endfile(text):
|
|
t = text.encode("utf8")
|
|
if t and (t[-1] != "\n"):
|
|
t += "\n"
|
|
d = os.path.dirname(f)
|
|
if not os.path.isdir(d):
|
|
os.makedirs(d)
|
|
savefile(f, text)
|
|
|
|
if source != None:
|
|
textsrc = "" if source == "" else readfile(source, filter)
|
|
if fname[0] == "/":
|
|
f = fname
|
|
else:
|
|
f = os.path.join(config.get("profile"), fname)
|
|
if os.path.isfile(f):
|
|
text0 = readfile(f, filter)
|
|
else:
|
|
assert source != None # The file must be present
|
|
text0 = textsrc
|
|
if not label:
|
|
label = _("Editing '%s'") % fname
|
|
ui.editor.start(label, endfile, text0, revert)
|
|
|
|
|
|
def browser(self, path):
|
|
self._browse("filebrowser", path)
|
|
|
|
|
|
def chroot(self, cmd, mounts=[]):
|
|
ip = config.ipath()
|
|
if ip != "/":
|
|
for m in mounts:
|
|
self.mount("/" + m, "%s/%s" % (ip, m), "--bind")
|
|
cmd = "chroot %s %s" % (ip, cmd)
|
|
|
|
s = supershell(cmd)
|
|
|
|
if ip != "/":
|
|
self.unmount(["%s/%s" % (ip, m) for m in mounts])
|
|
|
|
if s[0]:
|
|
if s[1]:
|
|
return s[1]
|
|
else:
|
|
return True
|
|
return False
|
|
|
|
|
|
def check_platform(self, report=True):
|
|
arch = Popen(["cat", config.ipath(".ARCH")],
|
|
stdout=PIPE, stderr=PIPE).communicate()[0].strip()
|
|
if arch:
|
|
if arch != config.get("platform"):
|
|
if report:
|
|
config_error(_("Platform error, installed system is %s")
|
|
% arch)
|
|
return False
|
|
else:
|
|
if report:
|
|
config_error(_("No installed system found"))
|
|
return False
|
|
return True
|
|
|
|
|
|
def enable_install(self):
|
|
withinstall = (config.get("install_path") != "/")
|
|
ui.command(":notebook.enableTab", 1, withinstall)
|
|
ui.command(":notebook.enableTab", 4, withinstall)
|
|
ui.command(":pacmanops.enable", withinstall
|
|
and self.check_platform(report=False))
|
|
|
|
|
|
def get_partitions(self):
|
|
"""Get a list of available partitions (only unmounted ones
|
|
are included).
|
|
"""
|
|
# First get a list of mounted devices
|
|
mounteds = []
|
|
fh = open("/etc/mtab")
|
|
for l in fh:
|
|
dev = l.split()[0]
|
|
if dev.startswith("/dev/sd"):
|
|
mounteds.append(dev)
|
|
fh.close()
|
|
# Get a list of partitions
|
|
partlist = []
|
|
for line in supershell("sfdisk -uM -l")[1]:
|
|
if line.startswith("/dev/sd"):
|
|
fields = line.replace("*", "").replace(" - ", " ? ")
|
|
fields = fields.replace("+", "").replace("-", "").split()
|
|
#debug("F5 '%s'" % fields[5])
|
|
if fields[5] in ["0", "5", "f", "82"]:
|
|
#debug("No")
|
|
continue # ignore uninteresting partitions
|
|
if fields[0] in mounteds:
|
|
continue # ignore mounted patitions
|
|
# Keep a tuple (partition, size in MiB)
|
|
partlist.append("%-12s %12s MiB" % (fields[0], fields[3]))
|
|
return partlist
|
|
|
|
|
|
|
|
def readfile(f, filter=None):
|
|
try:
|
|
fh = open(f)
|
|
r = fh.read()
|
|
fh.close()
|
|
return filter(r) if filter else r
|
|
except:
|
|
run_error(_("Couldn't read file '%s'") % f)
|
|
return ""
|
|
|
|
def savefile(f, d):
|
|
try:
|
|
fh = open(f, "w")
|
|
fh.write(d)
|
|
fh.close()
|
|
except:
|
|
run_error(_("Couldn't save file '%s'") % f)
|
|
|
|
|
|
def config_error(text):
|
|
ui.error(text, _("CONFIG ERROR"))
|
|
__builtin__.config_error = config_error
|
|
|
|
def run_error(text):
|
|
ui.error(text, _("BUILD ERROR"))
|
|
__builtin__.run_error = run_error
|
|
|
|
def fatal_error(text):
|
|
ui.error(text, _("FATAL ERROR"), fatal=True)
|
|
command.uiquit()
|
|
__builtin__.fatal_error = fatal_error
|
|
|
|
#---------------------------
|
|
# Catch all unhandled errors.
|
|
def errorTrap(type, value, tb):
|
|
etext = "".join(traceback.format_exception(type, value, tb))
|
|
ui.error(etext, _("This error could not be handled"), fatal=True)
|
|
command.uiquit()
|
|
|
|
sys.excepthook = errorTrap
|
|
#---------------------------
|
|
|
|
|
|
def simple_thread(func, *args):
|
|
t = threading.Thread(target=func, args=args)
|
|
t.start()
|
|
return t
|
|
|
|
|
|
re_mksquashfs = re.compile(r":-\[.*\](.* ([0-9]+)%)")
|
|
re_pacman = re.compile(r":-(?:\(([^/]+/[^)]+)\))?(.*?)\[([-#]+)\]\s+([0-9]+)%")
|
|
re_mkisofs = re.compile(r":-\d\d?\.\d\d%")
|
|
def ltstart():
|
|
"""A thread function for reading log lines from the log queue and
|
|
passing them to the logger.
|
|
"""
|
|
progress = ""
|
|
line0 = ""
|
|
text0 = ""
|
|
while True:
|
|
line = logqueue.get()
|
|
logqueue.task_done()
|
|
if line.startswith("L:/"):
|
|
# Quit logging
|
|
break
|
|
|
|
mp = re_pacman.match(line)
|
|
if mp:
|
|
#xfromy, text, bars, percent = m.groups()
|
|
text = mp.group(2).strip()
|
|
xfromy = mp.group(1)
|
|
t = text if xfromy else text[:20]
|
|
if (t != text0) and line0:
|
|
logline(line0)
|
|
line0 = "(%s) %s" % (xfromy, text) if xfromy else text
|
|
ui.progress.set(line[2:])
|
|
text0 = t
|
|
continue
|
|
|
|
# Filter the output of mksquashfs
|
|
ms = re_mksquashfs.match(line)
|
|
if ms:
|
|
percent = ms.group(2)
|
|
if progress != percent:
|
|
progress = percent
|
|
ui.progress.set("mksquashfs " + ms.group(1))
|
|
continue
|
|
|
|
# Test for mkisofs progress
|
|
if re_mkisofs.match(line):
|
|
ui.progress.set("mkisofs " + line[2:])
|
|
continue
|
|
|
|
ui.progress.set()
|
|
progress = ""
|
|
|
|
if line0:
|
|
logline(line0)
|
|
line0 = ""
|
|
text0 = ""
|
|
|
|
logline(line)
|
|
|
|
|
|
def logline(line):
|
|
ui.logger.addLine(line)
|
|
ui.progress.addLine(line)
|
|
logfile.write(line + '\n')
|
|
|
|
|
|
|
|
def usage():
|
|
errout(_(" Please see '%s/%s'\n for usage information.\n")
|
|
% (base_dir, "docs/html/larch_console.html"))
|
|
__builtin__.usage = usage
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
# Various ui toolkits could be supported, but at the moment there
|
|
# is only support for pyqt (apart from the console)
|
|
if (len(sys.argv) > 1) and (sys.argv[1].startswith("-c")):
|
|
from console import *
|
|
guiexec = None
|
|
else:
|
|
from gui import *
|
|
if (len(sys.argv) == 1) or (sys.argv[1] == "--pyqt"):
|
|
guiexec = "quip"
|
|
else:
|
|
errout(_("ERROR: Unsupported option - '%s'\n") % sys.argv[1])
|
|
errout(_("Start without arguments or with '--pyqt' to start pyqt gui.\n"))
|
|
errout(_("The command line interface is started with '-c':\n"))
|
|
usage()
|
|
sys.exit(1)
|
|
|
|
# The queue for log lines must be available before starting the supershell
|
|
__builtin__.logqueue = Queue()
|
|
|
|
# Set up ui
|
|
__builtin__.ui = Ui(guiexec)
|
|
# This starts the command interface
|
|
__builtin__.command = Command()
|
|
__builtin__.installation = Installation()
|
|
__builtin__.config = base.LarchConfig(os.environ["HOME"])
|
|
|
|
command.gui_init()
|
|
|
|
# Start log reader thread
|
|
logfile = open(config.working_dir + "/larch_log", "w")
|
|
lthread = simple_thread(ltstart)
|
|
|
|
__builtin__.supershell = command.supershell
|
|
|
|
signal.signal(signal.SIGINT, command.sigint)
|
|
|
|
command.run()
|
|
cc = ui.mainloop()
|
|
logfile.close()
|
|
exit(cc)
|