moinsetup

moinsetup.py

23:0ff99cb88bb1
2010-07-01 Paul Boddie Changed the static URL path to not conflict with the CGI URL path for Wiki installations using MoinMoin 1.8.x and earlier.
     1 #!/usr/bin/env python     2      3 from os.path import abspath, exists, extsep, isdir, join, normpath, split     4 from getpass import getpass     5 from glob import glob     6 import os     7 import sys     8 import shutil     9 import re    10     11 # Regular expressions for editing MoinMoin scripts and configuration files.    12     13 def compile_definition(name):    14     return re.compile(r"^(\s*)#*\s*(%s =).*$" % name, re.MULTILINE)    15     16 moin_cgi_prefix          = re.compile("^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE)    17 moin_cgi_wikiconfig      = re.compile("^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE)    18 moin_cgi_properties      = compile_definition("properties")    19 moin_cgi_fix_script_name = compile_definition("fix_script_name")    20 moin_cgi_force_cgi       = re.compile("^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE)    21     22 css_import_stylesheet    = re.compile("(\s*@import\s+[\"'])(.*?)([\"']\s*;)")    23     24 # Templates for Apache site definitions.    25     26 apache_site = """    27 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi"    28 """    29     30 apache_site_extra_moin18 = """    31 Alias %(static_url_path)s "%(htdocs_dir)s/"    32 """    33     34 # Limited hosting .htaccess definitions require the following settings to be    35 # configured in the main Apache configuration files:    36 #    37 # Options ExecCGI FollowSymLinks Indexes SymLinksIfOwnerMatch    38 # AllowOverride FileInfo Limit    39 # AddHandler cgi-script .cgi    40     41 apache_htaccess_combined_mod_rewrite = """    42 DirectoryIndex moin.cgi    43 RewriteEngine On    44 RewriteBase %(url_path)s    45 RewriteCond %%{REQUEST_FILENAME} !-f    46 RewriteCond %%{REQUEST_FILENAME} !-d    47 RewriteRule ^(.*) moin.cgi/$1 [PT,L,QSA]    48 """    49     50 # Utility functions.    51     52 def readfile(filename):    53     f = open(filename)    54     try:    55         return f.read()    56     finally:    57         f.close()    58     59 def writefile(filename, s):    60     f = open(filename, "w")    61     try:    62         f.write(s)    63     finally:    64         f.close()    65     66 def status(message):    67     print message    68     69 note = status    70 error = status    71     72 def format(s, indent):    73     return re.sub("\n\s+", "\n" + " " * indent, s)    74     75 # Classes.    76     77 class Configuration:    78     79     "A class representing the configuration."    80     81     special_names = ["site_name"]    82     83     def __init__(self, filename):    84         self.content = readfile(filename)    85         self.filename = filename    86     87     def get_pattern(self, name):    88     89         # Make underscores optional for certain names.    90     91         if name in self.special_names:    92             name = name.replace("_", "_?")    93     94         return compile_definition(name)    95     96     def set(self, name, value, count=None, raw=0):    97     98         """    99         Set the configuration parameter having the given 'name' with the given   100         'value', limiting the number of appropriately named parameters changed   101         to 'count', if specified.   102    103         If the configuration parameter of the given 'name' does not exist,   104         insert such a parameter at the end of the file.   105    106         If the optional 'raw' parameter is specified and set to a true value,   107         the provided 'value' is inserted directly into the configuration file.   108         """   109    110         if not self.replace(name, value, count, raw):   111             self.insert(name, value, raw)   112    113     def replace(self, name, value, count=None, raw=0):   114    115         """   116         Replace configuration parameters having the given 'name' with the given   117         'value', limiting the number of appropriately named parameters changed   118         to 'count', if specified.   119    120         If the optional 'raw' parameter is specified and set to a true value,   121         the provided 'value' is inserted directly into the configuration file.   122    123         Return the number of substitutions made.   124         """   125    126         if raw:   127             substitution = r"\1\2 %s" % value   128         else:   129             substitution = r"\1\2 %r" % value   130    131         pattern = self.get_pattern(name)   132    133         if count is None:   134             self.content, n = pattern.subn(substitution, self.content)   135         else:   136             self.content, n = pattern.subn(substitution, self.content, count=count)   137    138         return n   139    140     def insert(self, name, value, raw=0):   141    142         """   143         Insert the configuration parameter having the given 'name' and 'value'.   144    145         If the optional 'raw' parameter is specified and set to a true value,   146         the provided 'value' is inserted directly into the configuration file.   147         """   148    149         if raw:   150             insertion = "\n    %s = %s\n"   151         else:   152             insertion = "\n    %s = %r\n"   153    154         self.content += insertion % (name, value)   155    156     def close(self):   157    158         "Close the file, writing the content."   159    160         writefile(self.filename, self.content)   161    162 class Installation:   163    164     "A class for installing and initialising MoinMoin."   165    166     method_names = (   167         "setup",   168         "setup_wiki",   169         "install_moin",   170         "install_data",   171         "configure_moin",   172         "edit_moin_script",   173         "edit_moin_web_script",   174         "add_superuser",   175         "make_site_files",   176         "make_post_install_script",   177         "reconfigure_moin",   178         "install_theme",   179         "install_extension_package",   180         "install_plugins",   181         "install_actions",   182         "install_macros",   183         "install_theme_resources",   184         "edit_theme_stylesheet"   185         )   186    187     # NOTE: Need to detect Web server user.   188    189     web_user = "www-data"   190     web_group = "www-data"   191    192     # MoinMoin resources.   193    194     theme_master = "modernized"   195     extra_theme_css_files = ["SlideShow.css"]   196    197     def __init__(self, moin_distribution, prefix, web_app_dir, web_site_dir,   198         common_dir, url_path, superuser, site_name, front_page_name,   199         theme_default=None):   200    201         """   202         Initialise a Wiki installation using the following:   203    204           * moin_distribution - the directory containing a MoinMoin source   205                                 distribution   206           * prefix            - the installation prefix (equivalent to /usr)   207           * web_app_dir       - the directory where Web applications and scripts   208                                 reside (such as /home/www-user/cgi-bin)   209           * web_site_dir      - the directory where Web site definitions reside   210                                 (such as /etc/apache2/sites-available)   211           * common_dir        - the directory where the Wiki configuration,   212                                 resources and instance will reside (such as   213                                 /home/www-user/mywiki)   214           * url_path          - the URL path at which the Wiki will be made   215                                 available (such as / or /mywiki)   216           * superuser         - the name of the site's superuser (such as   217                                 "AdminUser")   218           * site_name         - the name of the site (such as "My Wiki")   219           * front_page_name   - the front page name for the site (such as   220                                 "FrontPage" or a specific name for the site)   221           * theme_default     - optional: the default theme (such as modern)   222         """   223    224         self.moin_distribution = moin_distribution   225         self.superuser = superuser   226         self.site_name = site_name   227         self.front_page_name = front_page_name   228         self.theme_default = theme_default   229    230         # NOTE: Support the detection of the Apache sites directory.   231    232         self.prefix, self.web_app_dir, self.web_site_dir, self.common_dir = \   233             map(abspath, (prefix, web_app_dir, web_site_dir, common_dir))   234    235         # Strip any trailing "/" from the URL path.   236    237         if url_path != "/" and url_path.endswith("/"):   238             self.url_path = url_path[:-1]   239         else:   240             self.url_path = url_path   241    242         # Define and create specific directories.   243    244         self.conf_dir = join(self.common_dir, "conf")   245         self.instance_dir = join(self.common_dir, "wikidata")   246    247         # Define useful directories.   248    249         self.prefix_site_packages = join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages")   250    251         # Find the version.   252    253         self.moin_version = self.get_moin_version()   254    255         # The static resources reside in different locations depending on the   256         # version of MoinMoin. Moreover, these resources may end up in a   257         # published directory for 1.8 installations where the Web server cannot   258         # be instructed to fetch the content from outside certain designated   259         # locations.   260    261         # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs   262    263         if self.moin_version.startswith("1.9"):   264             self.htdocs_dir = self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs")   265    266         # 1.8: moin/share/moin/htdocs (optionally copied to a Web directory)   267    268         else:   269             self.htdocs_dir_source = join(self.instance_dir, "share", "moin", "htdocs")   270    271             if self.limited_hosting():   272                 self.htdocs_dir = join(self.web_app_dir, self.get_static_identifier())   273             else:   274                 self.htdocs_dir = self.htdocs_dir_source   275    276     def get_moin_version(self):   277    278         "Inspect the MoinMoin package information, returning the version."   279    280         this_dir = os.getcwd()   281         os.chdir(self.moin_distribution)   282    283         try:   284             try:   285                 f = open("PKG-INFO")   286                 try:   287                     for line in f.xreadlines():   288                         columns = line.split()   289                         if columns[0] == "Version:":   290                             return columns[1]   291    292                     return None   293    294                 finally:   295                     f.close()   296    297             except IOError:   298                 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable)   299                 try:   300                     return f.read().strip()   301                 finally:   302                     f.close()   303         finally:   304             os.chdir(this_dir)   305    306     def get_static_identifier(self):   307    308         "Return the static URL/directory identifier for the Wiki."   309    310         return "moin_static%s" % self.moin_version.replace(".", "")   311    312     def get_plugin_directory(self, plugin_type):   313    314         "Return the directory for plugins of the given 'plugin_type'."   315    316         data_dir = join(self.conf_dir, "data")   317         return join(data_dir, "plugin", plugin_type)   318    319     def limited_hosting(self):   320    321         "Return whether limited Web hosting is being used."   322    323         return self.web_site_dir == self.web_app_dir   324    325     def ensure_directories(self):   326    327         "Make sure that all the directories are available."   328    329         for d in (self.conf_dir, self.instance_dir, self.web_app_dir, self.web_site_dir):   330             if not exists(d):   331                 os.makedirs(d)   332    333     def get_theme_directories(self, theme_name=None):   334    335         """   336         Return tuples of the form (theme name, theme directory) for all themes,   337         or for a single theme if the optional 'theme_name' is specified.   338         """   339    340         filenames = theme_name and [theme_name] or os.listdir(self.htdocs_dir)   341         directories = []   342    343         for filename in filenames:   344             theme_dir = join(self.htdocs_dir, filename)   345    346             if not exists(theme_dir) or not isdir(theme_dir):   347                 continue   348    349             directories.append((filename, theme_dir))   350    351         return directories   352    353     # Main methods.   354    355     def setup(self):   356    357         "Set up the installation."   358    359         self.ensure_directories()   360         self.install_moin()   361         self._setup_wiki()   362    363     def setup_wiki(self):   364    365         "Set up a Wiki without installing MoinMoin."   366    367         self.ensure_directories()   368         self.install_moin(data_only=1)   369         self._setup_wiki()   370    371     def _setup_wiki(self):   372    373         "Set up a Wiki without installing MoinMoin."   374    375         self.install_data()   376         self.configure_moin()   377         self.edit_moin_script()   378         self.edit_moin_web_script()   379         self.add_superuser()   380         self.make_site_files()   381         self.make_post_install_script()   382    383     def install_moin(self, data_only=0):   384    385         "Enter the distribution directory and run the setup script."   386    387         # NOTE: Possibly check for an existing installation and skip repeated   388         # NOTE: installation attempts.   389    390         this_dir = os.getcwd()   391         os.chdir(self.moin_distribution)   392    393         log_filename = "install-%s.log" % split(self.common_dir)[-1]   394    395         status("Installing MoinMoin in %s..." % self.prefix)   396    397         if data_only:   398             install_cmd = "install_data"   399             options = "--install-dir='%s'" % self.instance_dir   400         else:   401             install_cmd = "install"   402             options = "--prefix='%s' --install-data='%s' --record='%s'" % (self.prefix, self.instance_dir, log_filename)   403    404         os.system("python setup.py --quiet %s %s --force" % (install_cmd, options))   405    406         os.chdir(this_dir)   407    408     def install_data(self):   409    410         "Install Wiki data."   411    412         # The default wikiconfig assumes data and underlay in the same directory.   413    414         status("Installing data and underlay in %s..." % self.conf_dir)   415    416         for d in ("data", "underlay"):   417             source = join(self.moin_distribution, "wiki", d)   418             source_tar = source + os.path.extsep + "tar"   419             d_tar = source + os.path.extsep + "tar"   420    421             if os.path.exists(source):   422                 shutil.copytree(source, join(self.conf_dir, d))   423             elif os.path.exists(source_tar):   424                 shutil.copy(source_tar, self.conf_dir)   425                 os.system("tar xf %s -C %s" % (d_tar, self.conf_dir))   426             else:   427                 status("Could not copy %s into installed Wiki." % d)   428    429         # Copy static Web data if appropriate.   430    431         if not self.moin_version.startswith("1.9") and self.limited_hosting():   432    433             if not exists(self.htdocs_dir):   434                 os.mkdir(self.htdocs_dir)   435    436             for item in os.listdir(self.htdocs_dir_source):   437                 path = join(self.htdocs_dir_source, item)   438                 if isdir(path):   439                     shutil.copytree(path, join(self.htdocs_dir, item))   440                 else:   441                     shutil.copy(path, join(self.htdocs_dir, item))   442    443     def configure_moin(self):   444    445         "Edit the Wiki configuration file."   446    447         # NOTE: Single Wiki only so far.   448    449         # Static URLs seem to be different in MoinMoin 1.9.x.   450         # For earlier versions, reserve URL space alongside the Wiki.   451         # NOTE: MoinMoin usually uses an apparently common URL space associated   452         # NOTE: with the version, but more specific locations are probably   453         # NOTE: acceptable if less efficient.   454    455         if self.moin_version.startswith("1.9"):   456             self.static_url_path = self.url_path   457             url_prefix_static = "%r + url_prefix_static" % self.static_url_path   458         else:   459             # Add the static identifier to the URL path. For example:   460             # /         -> /moin_static187   461             # /hgwiki   -> /hgwiki/moin_static187   462    463             self.static_url_path = self.url_path + (self.url_path != "/" and "-" or "") + self.get_static_identifier()   464             url_prefix_static = "%r" % self.static_url_path   465    466         # Copy the Wiki configuration file from the distribution.   467    468         wikiconfig_py = join(self.conf_dir, "wikiconfig.py")   469         shutil.copyfile(join(self.moin_distribution, "wiki", "config", "wikiconfig.py"), wikiconfig_py)   470    471         status("Editing configuration from %s..." % wikiconfig_py)   472    473         # Edit the Wiki configuration file.   474    475         wikiconfig = Configuration(wikiconfig_py)   476    477         try:   478             wikiconfig.set("url_prefix_static", url_prefix_static, raw=1)   479             wikiconfig.set("superuser", [self.superuser])   480             wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser)   481    482             if not self.moin_version.startswith("1.9"):   483                 data_dir = join(self.conf_dir, "data")   484                 data_underlay_dir = join(self.conf_dir, "underlay")   485    486                 wikiconfig.set("data_dir", data_dir)   487                 wikiconfig.set("data_underlay_dir", data_underlay_dir)   488    489             self._configure_moin(wikiconfig)   490    491         finally:   492             wikiconfig.close()   493    494     def _configure_moin(self, wikiconfig):   495    496         """   497         Configure Moin, accessing the configuration file using 'wikiconfig'.   498         """   499    500         wikiconfig.set("site_name", self.site_name)   501         wikiconfig.set("page_front_page", self.front_page_name, count=1)   502    503         if self.theme_default is not None:   504             wikiconfig.set("theme_default", self.theme_default)   505    506     def edit_moin_script(self):   507    508         "Edit the moin script."   509    510         moin_script = join(self.prefix, "bin", "moin")   511    512         status("Editing moin script at %s..." % moin_script)   513    514         s = readfile(moin_script)   515         s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages)   516    517         writefile(moin_script, s)   518    519     def edit_moin_web_script(self):   520    521         "Edit and install CGI script."   522    523         # NOTE: CGI only so far.   524         # NOTE: Permissions should be checked.   525    526         if self.moin_version.startswith("1.9"):   527             moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.fcgi")   528         else:   529             moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.cgi")   530    531         moin_cgi_installed = join(self.web_app_dir, "moin.cgi")   532    533         status("Editing moin.cgi script from %s..." % moin_cgi)   534    535         s = readfile(moin_cgi)   536         s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s)   537         s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.conf_dir, s)   538    539         # Handle differences in script names when using limited hosting with   540         # URL rewriting.   541    542         if self.limited_hosting():   543             if self.moin_version.startswith("1.9"):   544                 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s)   545                 s = moin_cgi_force_cgi.sub(r"\1", s)   546             else:   547                 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s)   548    549         writefile(moin_cgi_installed, s)   550         os.system("chmod a+rx '%s'" % moin_cgi_installed)   551    552     def add_superuser(self):   553    554         "Add the superuser account."   555    556         moin_script = join(self.prefix, "bin", "moin")   557    558         print "Creating superuser", self.superuser, "using..."   559         email = raw_input("E-mail address: ")   560         password = getpass("Password: ")   561    562         path = os.environ.get("PYTHONPATH", "")   563    564         if path:   565             os.environ["PYTHONPATH"] = path + ":" + self.conf_dir   566         else:   567             os.environ["PYTHONPATH"] = self.conf_dir   568    569         os.system(moin_script + " account create --name='%s' --email='%s' --password='%s'" % (self.superuser, email, password))   570    571         if path:   572             os.environ["PYTHONPATH"] = path   573         else:   574             del os.environ["PYTHONPATH"]   575    576     def make_site_files(self):   577    578         "Make the Apache site files."   579    580         # NOTE: Using local namespace for substitution.   581    582         # Where the site definitions and applications directories are different,   583         # use a normal site definition.   584    585         if not self.limited_hosting():   586    587             site_def = join(self.web_site_dir, self.site_name)   588    589             s = apache_site % self.__dict__   590    591             if not self.moin_version.startswith("1.9"):   592                 s += apache_site_extra_moin18 % self.__dict__   593    594         # Otherwise, use an .htaccess file.   595    596         else:   597             site_def = join(self.web_site_dir, ".htaccess")   598    599             s = apache_htaccess_combined_mod_rewrite % self.__dict__   600    601         status("Writing Apache site definitions to %s..." % site_def)   602    603         writefile(site_def, s)   604    605     def make_post_install_script(self):   606    607         "Write a post-install script with additional actions."   608    609         this_user = os.environ["USER"]   610         postinst_script = "moinsetup-post.sh"   611    612         s = "#!/bin/sh\n"   613    614         for d in ("data", "underlay"):   615             s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, join(self.conf_dir, d))   616             s += "chmod -R g+w '%s'\n" % join(self.conf_dir, d)   617    618         if not self.moin_version.startswith("1.9"):   619             s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, self.htdocs_dir)   620    621         writefile(postinst_script, s)   622         os.chmod(postinst_script, 0755)   623         note("Run %s as root to set file ownership and permissions." % postinst_script)   624    625     # Accessory methods.   626    627     def reconfigure_moin(self, name=None, value=None, raw=0):   628    629         "Edit the installed Wiki configuration file."   630    631         wikiconfig_py = join(self.conf_dir, "wikiconfig.py")   632    633         status("Editing configuration from %s..." % wikiconfig_py)   634    635         wikiconfig = Configuration(wikiconfig_py)   636    637         try:   638             # Perform default configuration.   639    640             if name is None and value is None:   641                 self._configure_moin(wikiconfig)   642             else:   643                 wikiconfig.set(name, value, raw=raw)   644    645         finally:   646             wikiconfig.close()   647    648     def install_theme(self, theme_dir):   649    650         "Install Wiki theme provided in the given 'theme_dir'."   651    652         theme_dir = normpath(theme_dir)   653         theme_name = split(theme_dir)[-1]   654         theme_module = join(theme_dir, theme_name + extsep + "py")   655    656         plugin_theme_dir = self.get_plugin_directory("theme")   657    658         # Copy the theme module.   659    660         status("Copying theme module to %s..." % plugin_theme_dir)   661    662         shutil.copy(theme_module, plugin_theme_dir)   663    664         # Copy the resources.   665    666         resources_dir = join(self.htdocs_dir, theme_name)   667    668         if not exists(resources_dir):   669             os.mkdir(resources_dir)   670    671         status("Copying theme resources to %s..." % resources_dir)   672    673         for d in ("css", "img"):   674             target_dir = join(resources_dir, d)   675             if exists(target_dir):   676                 status("Replacing %s..." % target_dir)   677                 shutil.rmtree(target_dir)   678             shutil.copytree(join(theme_dir, d), target_dir)   679    680         # Copy additional resources from other themes.   681    682         resources_source_dir = join(self.htdocs_dir, self.theme_master)   683         target_dir = join(resources_dir, "css")   684    685         status("Copying resources from %s..." % resources_source_dir)   686    687         for css_file in self.extra_theme_css_files:   688             css_file_path = join(resources_source_dir, "css", css_file)   689             if exists(css_file_path):   690                 shutil.copy(css_file_path, target_dir)   691    692         note("Don't forget to add theme resources for extensions for this theme.")   693         note("Don't forget to edit theme stylesheets for any extensions.")   694    695     def install_extension_package(self, extension_dir):   696    697         "Install any libraries from 'extension_dir' using a setup script."   698    699         this_dir = os.getcwd()   700         os.chdir(extension_dir)   701         os.system("python setup.py install --prefix=%s" % self.prefix)   702         os.chdir(this_dir)   703    704     def install_plugins(self, plugins_dir, plugin_type):   705    706         """   707         Install Wiki actions provided in the given 'plugins_dir' of the   708         specified 'plugin_type'.   709         """   710    711         plugin_target_dir = self.get_plugin_directory(plugin_type)   712    713         # Copy the modules.   714    715         status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir))   716    717         for module in glob(join(plugins_dir, "*%spy" % extsep)):   718             shutil.copy(module, plugin_target_dir)   719    720     def install_actions(self, actions_dir):   721    722         "Install Wiki actions provided in the given 'actions_dir'."   723    724         self.install_plugins(actions_dir, "action")   725    726     def install_macros(self, macros_dir):   727    728         "Install Wiki macros provided in the given 'macros_dir'."   729    730         self.install_plugins(macros_dir, "macro")   731    732     def install_theme_resources(self, theme_resources_dir, theme_name=None):   733    734         """   735         Install theme resources provided in the given 'theme_resources_dir'. If   736         a specific 'theme_name' is given, only that theme will be given the   737         specified resources.   738         """   739    740         for theme_name, theme_dir in self.get_theme_directories(theme_name):   741    742             # Copy the resources.   743    744             copied = 0   745    746             for d in ("css", "img"):   747                 source_dir = join(theme_resources_dir, d)   748                 target_dir = join(theme_dir, d)   749    750                 if not exists(target_dir):   751                     continue   752    753                 for resource in glob(join(source_dir, "*%s*" % extsep)):   754                     shutil.copy(resource, target_dir)   755                     copied = 1   756    757             if copied:   758                 status("Copied theme resources into %s..." % theme_dir)   759    760         note("Don't forget to edit theme stylesheets for any extensions.")   761    762     def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None):   763    764         """   765         Edit the given 'theme_stylesheet', ensuring (or removing) a reference to   766         the 'imported_stylesheet' according to the given 'action' (optional,   767         defaulting to "ensure"). If a specific 'theme_name' is given, only that   768         theme will be affected.   769         """   770    771         if action == "ensure":   772             ensure = 1   773         elif action == "remove":   774             ensure = 0   775         else:   776             error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action)   777             return   778    779         for theme_name, theme_dir in self.get_theme_directories(theme_name):   780    781             # Locate the resources.   782    783             css_dir = join(theme_dir, "css")   784    785             if not exists(css_dir):   786                 continue   787    788             theme_stylesheet_filename = join(css_dir, theme_stylesheet)   789             imported_stylesheet_filename = join(css_dir, imported_stylesheet)   790    791             if not exists(theme_stylesheet_filename):   792                 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name))   793                 continue   794    795             if not exists(imported_stylesheet_filename):   796                 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name))   797                 continue   798    799             # Edit the resources.   800    801             s = readfile(theme_stylesheet_filename)   802             after_point = 0   803    804             for stylesheet_import in css_import_stylesheet.finditer(s):   805                 before, filename, after = stylesheet_import.groups()   806                 before_point, after_point = stylesheet_import.span()   807    808                 # Test the import for a reference to the requested imported   809                 # stylesheet.   810    811                 if filename == imported_stylesheet:   812                     if ensure:   813                         break   814                     else:   815                         if s[after_point:after_point+1] == "\n":   816                             after_point += 1   817                         s = "%s%s" % (s[:before_point], s[after_point:])   818    819                         status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))   820                         writefile(theme_stylesheet_filename, s)   821                         break   822    823             # Where no import references the imported stylesheet, insert a   824             # reference into the theme stylesheet.   825    826             else:   827                 if ensure:   828    829                     # Assume that the stylesheet can follow other imports.   830    831                     if s[after_point:after_point+1] == "\n":   832                         after_point += 1   833                     s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:])   834    835                     status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))   836                     writefile(theme_stylesheet_filename, s)   837    838 # Command line option syntax.   839    840 syntax_description = "<argument> ... --method=METHOD [ <method-argument> ... ]"   841    842 # Main program.   843    844 if __name__ == "__main__":   845     import sys, cmdsyntax   846    847     # Check the command syntax.   848    849     syntax = cmdsyntax.Syntax(syntax_description)   850     try:   851         matches = syntax.get_args(sys.argv[1:])   852         args = matches[0]   853    854         # Obtain as many arguments as needed for the configuration.   855    856         arguments = args["argument"]   857         method_arguments = args.get("method-argument", [])   858    859         # Attempt to initialise the configuration.   860    861         installation = Installation(*arguments)   862    863     except (IndexError, TypeError):   864         print "Syntax:"   865         print sys.argv[0], syntax_description   866         print   867         print "Arguments:"   868         print Installation.__init__.__doc__   869         print   870         print "Methods:"   871         print   872         for method_name in Installation.method_names:   873             doc = getattr(Installation, method_name).__doc__.strip()   874             print "%-30s%-s" % (method_name, format(doc, 30))   875         print   876         sys.exit(1)   877    878     # Obtain and perform the method.   879    880     if args.has_key("method"):   881         method = getattr(installation, args["method"])   882     else:   883         method = installation.setup   884    885     method(*method_arguments)   886    887 # vim: tabstop=4 expandtab shiftwidth=4