moinsetup

moinsetup.py

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