#!/usr/bin/env python
import os
import re
from optparse import OptionParser
from sys import stderr
from subprocess import Popen, PIPE
from warnings import warn
if "SAGE_ROOT" in os.environ:
sage = os.environ["SAGE_ROOT"]+"/sage"
else:
sage = "sage"
##############################################################################
# Command line option handling
usage = r"""usage: %prog [options] command
list of commands:
config show current configuration (sage command, path, version, ...)
install install the sage-combinat patches
update update to the latest sage-combinat patches
upgrade upgrade sage and update to the latest sage-combinat patches
status show changed files in the working directory and in the patch queue
qselect choose appropriate guards for the current version of sage"""
parser = OptionParser(usage=usage)
parser.add_option("-b", "--branch", dest="branch",
help="Use sage-BRANCH instead of sage-combinat",
metavar="BRANCH", default = "combinat", type="string")
def set_sage_command(option, opt, value, parser):
global sage
if sage == value:
return
setattr(parser.values, option.dest, value)
parser.add_option("--sage", action = "callback", callback = set_sage_command,
help="Specify and store the sage command line",
metavar="/opt/bin/sage", default = sage, type="string")
parser.add_option("-f", "--force", action="store_true", dest="force",
help="Force proceeding")
parser.set_defaults(verbose=True)
parser.add_option("-v", action="store_true", dest="verbose",
help="Print status messages to stdout")
parser.add_option("-q", "--quiet",
action="store_false", dest="verbose",
help="Don't print status messages to stdout")
parser.add_option("-s", "--server", dest="server",
help="Set the URL for the sage-combinat server",
metavar="http://combinat.sagemath.org/patches/",
default="http://combinat.sagemath.org/patches/", type="string")
parser.add_option("-n", dest="n", action="store_true",
help="After qselect: disable all previous non version guards")
(options, args) = parser.parse_args()
##############################################################################
# Utilities
def info(mesg):
global options
if options.verbose:
print >> stderr, mesg
def error(s):
print >> stderr, "Error: "+s
parser.print_help()
exit(0)
def system(command):
info(" "+command)
ret = os.system(command)
if not ret == 0:
print >> stderr, "Abort"
exit(ret)
def get_sage_root():
r"""
Returns the root directory of the sage installation (e.g. /opt/sage)
"""
global sage
if os.environ.has_key("SAGE_ROOT"):
return os.environ["SAGE_ROOT"]
# Query the environment variable via sage -sh
try:
sage_root = Popen(["echo 'echo ROOT${SAGE_ROOT}ROOT' | "+sage+" -sh"], stdout=PIPE, shell=True).communicate()[0]
except OSError, e:
error("Could not start sage"+str(e))
return re.split("ROOT", sage_root)[1]
"""
A regexp matching Sage version strings
EXAMPLES::
sage: re.search(version_regexp, "Sage Version 5.0").group()
'5.0'
sage: re.search(version_regexp, "Sage Version 5.4.2").group()
'5.4.2'
sage: re.search(version_regexp, "Sage Version 5.3.1.alpha3").group()
'5.3.1.alpha3'
sage: re.search(version_regexp, "Sage Version 5.3.1.alpha3 blah blah").group()
'5.3.1.alpha3'
sage: re.search(version_regexp, "Sage Version 5.3.beta2 blah blah").group()
'5.3.beta2'
sage: re.search(version_regexp, "Sage Version 5.rc0 blah blah").group()
'5.rc0'
sage: re.match(version_regexp, "Sage Version 5.rc0 blah blah")
sage: re.match(version_regexp, "5.rc0 blah").group()
'5.rc0'
sage: re.match(version_regexp+"$", "4_3_3:")
sage: re.match(version_regexp, "4_3_3:")
'4_3_3'
"""
version_regexp = '(\d+(\.|_))*(\d+)((\.|_)(alpha|beta|rc)(\d+))?'
def get_sage_version():
global sage
# Looking up for sage and sage root directory
try:
sage_version = Popen([sage+" --version"], stdout=PIPE, shell=True).communicate()[0]
except OSError, e:
error("Could not start sage"+str(e))
match = re.search(version_regexp, sage_version)
if match is None:
error("Cannot determine Sage version number from"+sage_version)
else:
version = match.group()
return version
def encode_sage_version_for_comparison(version):
"""
Encodes a version of Sage as a list for comparison purposes
INPUT:
- ``version`` -- a string
sage: encode_sage_version_for_comparison("4.2")
[4, 2, 0]
sage: encode_sage_version_for_comparison("5.0.beta3")
[5, 0, -3, 3, 0]
sage: encode_sage_version_for_comparison("4.2.1.rc0")
[4, 2, 1, -1, 0, 0]
sage: encode_sage_version_for_comparison("4.0.alpha3")
[4, 0, -3, 3, 0]
The only purpose of this encoding is that Python's standard
(lexicographic) comparison of lists implements the comparison of
Sage versions.
EXAMPLES::
sage: e = encode_sage_version_for_comparison
sage: e("4.2") < e("4.2.1")
True
sage: e("4.2.1") < e("4.2.2")
True
sage: e("4.2.rc0") < e("4.2")
True
sage: e("4.7.2.rc0") < e("4.7.2.rc2")
True
sage: e("4.7.2.beta2") < e("4.7.2")
True
sage: e("4.7.2.beta2") < e("4.7.2.beta3")
True
sage: e("4.7.2.beta10") < e("4.7.2.rc0")
True
"""
version = version.replace("alpha", "-3.")
version = version.replace("beta", "-2.")
version = version.replace("rc", "-1.")
return [int(s) for s in re.split("\.|_", version)]+[0]
def cmp_sage_versions(version1, version2):
"""
Compares two Sage versions given as strings
EXAMPLES::
sage: cmp_sage_versions("4.2", "4.2.1")
-1
sage: cmp_sage_versions("4.2.1", "4.2.2")
-1
sage: cmp_sage_versions("4.2.rc0", "4.2")
-1
sage: cmp_sage_versions("4.7.2.rc0", "4.7.2.rc2")
-1
sage: cmp_sage_versions("4.7.2.beta2", "4.7.2")
-1
sage: cmp_sage_versions("4.7.2.beta2", "4.7.2.beta3")
-1
sage: cmp_sage_versions("4.7.2.beta10", "4.7.2.rc0")
-1
sage: cmp_sage_versions("4.7.2.beta10", "4.7.2.beta10")
0
"""
return cmp(encode_sage_version_for_comparison(version1), encode_sage_version_for_comparison(version2))
def cd_to_combinat():
global sage_combinat_root
info("Switching to sage combinat root directory: %s"%sage_combinat_root)
os.chdir(sage_combinat_root)
# TODO: organize this as in sage.misc.sg
def hg_query(command, dir):
r"""
Runs hg in the directory dir, and returns its output as a string
"""
cd_to_combinat() # make sure we are in the sage combinat directory
return Popen(["cd %s && %s %s"%(dir,hg,command)], stdout=PIPE, shell=True).communicate()[0].rstrip()
def hg_status():
global sage_combinat_root
return hg_query("status", sage_combinat_root)
def hg_qstatus():
global sage_combinat_patch_queue
return hg_query("status", sage_combinat_patch_queue)
def hg_qtop():
global sage_combinat_root
return hg_query("qtop", sage_combinat_root)
def hg_series():
global sage_combinat_root
return hg_query("qseries", sage_combinat_root)
def hg_all_patches():
return re.split("\r\n|\r|\n",hg_series())
def hg_qnext():
global sage_combinat_root
return hg_query("qnext", sage_combinat_root)
def hg_all_guards():
return re.split("\r\n|\r|\n",
hg_query("qselect -s", sage_combinat_root))
def hg_active_guards():
# hg qselect -v returns all the active guards, one per line
# the first line is always "active guards" or "no active guards"
return re.split("\r\n|\r|\n",
hg_query("qselect -v", sage_combinat_root))[1:]
def hg_are_all_patch_applied():
return hg_qnext() == "All patches applied"
def hg_are_no_patch_applied():
return hg_qtop() == "No patches applied"
def check_for_no_diff(check_patch_queue=True):
if hg_status() != "":
info("There are local changes; aborting")
info("Please qrefresh or discard your changes before proceeding")
info(hg_status())
exit(1)
if check_patch_queue and hg_qstatus() != "":
info("There are uncommited patches:")
info(hg_qstatus())
if not options.force:
info("Use option --force to proceed anyway")
exit(1)
def qselect_backward_compatibility_patches(guards = []):
global sage_version, options
r"""
Selects the appropriate guards for this version of sage
e.g. if we are running sage 3.0.2, then we want to apply all
the patches which are guarded by 3_0_3, 3_0_4, ...
"""
# FIXME: how to change the guards on a one by one basis
non_version_guards = filter(lambda guard: re.match(version_regexp, guard) is None, hg_active_guards())
info("Current non version guards: %s"%" ".join(non_version_guards))
if options.n:
non_version_guards = []
non_version_guards = non_version_guards + guards
info("Updated non version guards: %s"%" ".join(non_version_guards))
def is_newversion_guard(guard):
"""
Returns true if this is a guard for version X (like 4_3_3 or 5_0_beta4) with X > sage_version
"""
if not re.match(version_regexp+"$", guard):
if re.match(version_regexp, guard):
# Catch erroneous guards like 4_3_3:
message = "Invalid version guard in the mercurial queue: %s"%guard
if options.force:
warn(message)
else:
error(message)
return False
if cmp_sage_versions(guard, sage_version) > 0:
info("Keep backward compatibility patches for sage "+re.sub("_",".",guard))
return True
else:
info("Skip backward compatibility patches for sage "+re.sub("_",".",guard))
return False
version_guards = filter(is_newversion_guard,
map(lambda guard: guard[1:], # get rid of leading "+"
hg_all_guards()))
info("Updating guards")
system(hg+" qselect -q -n")
system(hg+" qselect "+
" ".join(non_version_guards + version_guards))
def update(update_from_sage_main = False):
r"""
High level operation to update the sage-combinat patches:
- pop all patches
- update the patch queue
- push back the patches
- rebuild
"""
check_for_no_diff()
info("Storing top applied patch")
if hg_are_all_patch_applied() or hg_are_no_patch_applied():
qtop = "-a"
else:
qtop = hg_qtop()
info("Unapplying all the patches")
system(hg+" qpop -a")
if update_from_sage_main:
info("Pulling the new version of Sage from the local main repository")
system(hg+" pull -u ../sage-main")
info("Pulling the new version of the patches from the patch server")
system("(cd .hg/patches ; "+hg+" pull -u %s)"%options.server)
qselect_backward_compatibility_patches()
# Revert to the formerly applied top patch.
# If this was the topmost patch, or if this patch does not
# exist anymore, then apply all patches
if qtop in hg_all_patches():
# Revert up to the formerly applied top patch.
system(hg+" qpush %s"%qtop)
info("Reapplying the patches up to %s"%qtop)
else:
if qtop != "-a":
info("Warning: the former top patch %s does not exist anymore"%qtop)
info("Applying all patches")
system(hg+" qpush -a")
info("Rebuilding")
system(sage+" -b %s"%branch)
##############################################################################
sage = options.sage
hg = """hg --config 'extensions.hgext.mq=' --config 'ui.username="sage-combinat script"'"""
sage_root = get_sage_root()
sage_version = get_sage_version()
##############################################################################
if len(args) == 0:
error("command required")
branch = options.branch
sage_combinat_root = os.path.join(sage_root, "devel", "sage-"+branch)
sage_combinat_hg = os.path.join(sage_combinat_root, ".hg")
sage_combinat_patch_queue = os.path.join(sage_combinat_hg, "patches")
if args[0] == "config":
info("sage command: "+sage)
info("sage version: "+sage_version)
info("sage-combinat branch: "+branch)
info("sage-combinat server: "+options.server)
info("")
info("sage root: "+sage_root)
info("sage-combinat root: "+sage_combinat_root)
info("sage-combinat hg repository: "+sage_combinat_hg)
info("sage-combinat patch queue: "+sage_combinat_patch_queue)
elif args[0] == "install":
if os.path.exists(sage_combinat_root):
if os.path.exists(sage_combinat_patch_queue):
error("sage-combinat apparently already installed in %s"%sage_combinat_hg)
else:
info("Detected a partial (broken?) installation of sage-combinat in %s"%sage_combinat_root)
if not options.force:
info("Use option --force proceed anyway and try to fix it")
exit(1)
else:
info("Creating sage-%s branch:"%branch)
system(sage+" -b main") # This makes sure we are cloning the main branch!!!
system(sage+" -clone %s"%branch)
info("Done")
assert(os.path.exists(sage_combinat_hg))
assert(not os.path.exists(sage_combinat_patch_queue))
check_for_no_diff(check_patch_queue=False)
info("Uploading sage-combinat patches into .hg/patches:")
system("cd .hg/; "+hg+" clone %s patches"%options.server)
qselect_backward_compatibility_patches()
info("Applying all the patches")
system(hg+" qpush -a")
info("Switching to %s branch and rebuilding"%branch)
system(sage+" -b %s"%branch)
info("Finished installation of Sage-combinat patches!")
elif args[0] == "update":
update()
elif args[0] == "upgrade":
check_for_no_diff()
info("Upgrading sage")
system(sage+" -upgrade")
# Update Sage's version to apply the appropriate patches!
# (we assume sage's root has not changed)
sage_version = get_sage_version()
update(update_from_sage_main = True)
elif args[0] == "status":
info("Top patch applied: %s"%hg_qtop())
info("Are all patches applied: %s "%str(hg_are_all_patch_applied()))
info("Changed files in the sage-combinat directory:")
info(hg_status())
info("Changed files in the sage-combinat patch directory:")
info(hg_qstatus())
elif args[0] == "qselect":
qselect_backward_compatibility_patches(args[1:])
else:
error("unknown command "+args[0])