moinsetup

moinsetup.py

46:f160eeb5794b
2011-09-17 Paul Boddie Changed the page package installation script environment to include specific configuration file locations. Changed extension package installation to install libraries only in the calculated site-packages location.
     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 import tempfile    31     32 __version__ = "0.3"    33     34 # Regular expressions for editing MoinMoin scripts and configuration files.    35     36 def compile_definition(name):    37     return re.compile(r"^(\s*)#*\s*(%s =)\s*(.*)$" % name, re.MULTILINE)    38     39 wikiconfig_import        = re.compile(r"^(\s*)#*\s*"    40                             r"(from\s+)(?P<module>\S+)"    41                             r"(\s+import\s+)(?P<names>(?:\S|[^,\s])+(?:\s*,\s*(?:\S|[^,\s])+)*)"    42                             r"(\s*)$", re.MULTILINE)    43     44 moin_cgi_prefix          = re.compile(r"^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE)    45 moin_cgi_wikiconfig      = re.compile(r"^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE)    46 moin_cgi_properties      = compile_definition("properties")    47 moin_cgi_fix_script_name = compile_definition("fix_script_name")    48 moin_cgi_force_cgi       = re.compile(r"^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE)    49     50 css_import_stylesheet    = re.compile(r"(\s*@import\s+[\"'])(.*?)([\"']\s*;)")    51     52 # Templates for Apache site definitions.    53     54 apache_site = """    55 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi"    56 """    57     58 apache_site_extra = """    59 Alias %(static_url_path)s "%(htdocs_dir)s/"    60 """    61     62 # Limited hosting .htaccess definitions require the following settings to be    63 # configured in the main Apache configuration files:    64 #    65 # Options ExecCGI FollowSymLinks Indexes SymLinksIfOwnerMatch    66 # AllowOverride FileInfo Limit    67 # AddHandler cgi-script .cgi    68     69 apache_htaccess_combined_mod_rewrite = """    70 DirectoryIndex moin.cgi    71 RewriteEngine On    72 RewriteBase %(url_path)s    73 RewriteCond %%{REQUEST_FILENAME} !-f    74 RewriteCond %%{REQUEST_FILENAME} !-d    75 RewriteRule ^(.*) moin.cgi/$1 [PT,L,QSA]    76 """    77     78 # Post-setup templates.    79     80 postsetup_setfacl = """#!/bin/sh    81     82 find '%(common_dir)s/data' -type f | xargs setfacl -m u:%(web_user)s:rw    83 find '%(common_dir)s/data' -type d | xargs setfacl -m u:%(web_user)s:rwx    84 find '%(common_dir)s/underlay' -type f | xargs setfacl -m u:%(web_user)s:rw    85 find '%(common_dir)s/underlay' -type d | xargs setfacl -m u:%(web_user)s:rwx    86 """    87     88 postsetup_setfacl_extra = """    89 find '%(htdocs_dir)s' -type f | xargs setfacl -m u:%(web_user)s:r    90 find '%(htdocs_dir)s' -type d | xargs setfacl -m u:%(web_user)s:rx    91 """    92     93 postsetup_chown_chmod = """#!/bin/sh    94     95 chown -R %(this_user)s.%(web_group)s '%(common_dir)s/data'    96 chown -R %(this_user)s.%(web_group)s '%(common_dir)s/underlay'    97 chmod -R g+w '%(common_dir)s/data'    98 chmod -R g+w '%(common_dir)s/underlay'    99 """   100    101 postsetup_chown_extra = """   102 chown -R %(this_user)s.%(web_group)s '%(htdocs_dir)s'   103 """   104    105 # Utility functions.   106    107 def readfile(filename):   108     f = open(filename)   109     try:   110         return f.read()   111     finally:   112         f.close()   113    114 def writefile(filename, s):   115     f = open(filename, "w")   116     try:   117         f.write(s)   118     finally:   119         f.close()   120    121 def status(message):   122     print message   123    124 note = status   125 error = status   126    127 def format(s, indent):   128     return re.sub("\n\s+", "\n" + " " * indent, s)   129    130 # Classes.   131    132 class SetupException(Exception):   133    134     "An exception indicating a problem with a setup action."   135    136     pass   137    138 class Configuration:   139    140     "A class representing the configuration."   141    142     special_names = ["site_name"]   143    144     def __init__(self, filename):   145         self.content = readfile(filename)   146         self.filename = filename   147    148     def get_pattern(self, name):   149    150         # Make underscores optional for certain names.   151    152         if name in self.special_names:   153             name = name.replace("_", "_?")   154    155         return compile_definition(name)   156    157     def get(self, name):   158    159         """   160         Return the raw value of the last definition having the given 'name'.   161         """   162    163         pattern = self.get_pattern(name)   164         results = [match.group(3) for match in pattern.finditer(self.content)]   165         if results:   166             return results[-1]   167         else:   168             return None   169    170     def set(self, name, value, count=None, raw=0):   171    172         """   173         Set the configuration parameter having the given 'name' with the given   174         'value', limiting the number of appropriately named parameters changed   175         to 'count', if specified.   176    177         If the configuration parameter of the given 'name' does not exist,   178         insert such a parameter at the end of the file.   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 not self.replace(name, value, count, raw):   185             self.insert(name, value, raw)   186    187     def replace(self, name, value, count=None, raw=0):   188    189         """   190         Replace configuration parameters having the given 'name' with the given   191         'value', limiting the number of appropriately named parameters changed   192         to 'count', if specified.   193    194         If the optional 'raw' parameter is specified and set to a true value,   195         the provided 'value' is inserted directly into the configuration file.   196    197         Return the number of substitutions made.   198         """   199    200         if raw:   201             substitution = r"\1\2 %s" % value   202         else:   203             substitution = r"\1\2 %r" % value   204    205         pattern = self.get_pattern(name)   206    207         if count is None:   208             self.content, n = pattern.subn(substitution, self.content)   209         else:   210             self.content, n = pattern.subn(substitution, self.content, count=count)   211    212         return n   213    214     def insert(self, name, value, raw=0):   215    216         """   217         Insert the configuration parameter having the given 'name' and 'value'.   218    219         If the optional 'raw' parameter is specified and set to a true value,   220         the provided 'value' is inserted directly into the configuration file.   221         """   222    223         if raw:   224             insertion = "%s = %s"   225         else:   226             insertion = "%s = %r"   227    228         self.insert_text(insertion % (name, value))   229    230     def insert_text(self, text):   231    232         "Insert the given 'text' at the end of the configuration."   233    234         if not self.content.endswith("\n"):   235             self.content += "\n"   236         self.content += "    %s\n" % text   237    238     def set_import(self, imported_module, imported_names):   239    240         """   241         Set up an import of the given 'imported_module' exposing the given   242         'imported_names'.   243         """   244    245         s = self.content   246         after_point = 0   247         first_point = None   248    249         for module_import in wikiconfig_import.finditer(s):   250             before, from_keyword, module, import_keyword, names, after = module_import.groups()   251             before_point, after_point = module_import.span()   252    253             if first_point is None:   254                 first_point = after_point   255    256             names = [name.strip() for name in names.split(",")]   257    258             # Test the import for a reference to the requested imported module.   259    260             if imported_module == module:   261                 for name in imported_names:   262                     if name not in names:   263                         names.append(name)   264    265                 self.content = s[:before_point] + (   266                     "%s%s%s%s%s%s" % (before, from_keyword, module, import_keyword, ", ".join(names), after)   267                     ) + s[after_point:]   268                 break   269    270         # Where no import references the imported module, insert a reference   271         # into the configuration.   272    273         else:   274             # Add the import after the first one.   275    276             if first_point is not None:   277                 self.content = s[:first_point] + ("\nfrom %s import %s" % (imported_module, ", ".join(imported_names))) + s[first_point:]   278    279     def close(self):   280    281         "Close the file, writing the content."   282    283         writefile(self.filename, self.content)   284    285 class Installation:   286    287     "A class for installing and initialising MoinMoin."   288    289     method_names = (   290         "show_config",   291         "setup",   292         "setup_wiki",   293         "install_moin",   294         "install_data",   295         "install_static_data",   296         "configure_moin",   297         "edit_moin_script",   298         "edit_moin_web_script",   299         "add_superuser",   300         "make_site_files",   301         "make_post_install_script",   302         "reconfigure_moin",   303         "set_auth_method",   304    305         # Post-installation activities.   306    307         "install_theme",   308         "install_extension_package",   309         "install_plugins",   310         "install_actions",   311         "install_macros",   312         "install_parsers",   313         "install_theme_resources",   314         "edit_theme_stylesheet",   315    316         # Other activities.   317    318         "make_page_package",   319         "install_page_package",   320         )   321    322     source_config_names = (   323         "moin_distribution", "prefix", "site_packages", "prefix_site_packages",   324         "htdocs_dir_source"   325         )   326    327     instance_config_names = (   328         "common_dir", "farm_config", "site_config",   329         "site_name", "site_identifier",   330         "front_page_name", "superuser", "theme_default", "htdocs_dir"   331         )   332    333     site_config_names = (   334         "web_app_dir", "web_site_dir", "web_static_dir",   335         "url_path", "static_url_path"   336         )   337    338     # NOTE: Need to detect Web server user.   339    340     web_user = "www-data"   341     web_group = "www-data"   342    343     # MoinMoin resources.   344    345     theme_master = "modernized"   346     extra_theme_css_files = ["SlideShow.css"]   347    348     def __init__(self, moin_distribution=None, prefix=None,   349         site_packages=None, web_app_dir=None, web_static_dir=None, web_site_dir=None,   350         common_dir=None, farm_config=None, site_config=None, url_path=None,   351         superuser=None, site_name=None, site_identifier=None, front_page_name=None,   352         theme_default=None):   353    354         """   355         Initialise a Wiki installation using the following installation   356         settings:   357    358           * moin_distribution - the directory containing a MoinMoin source   359                                 distribution (can be omitted)   360           * prefix            - the installation prefix (equivalent to /usr)   361           * site_packages     - optional: the location of the Python   362                                 site-packages directory if outside the 'prefix'   363                                 (overrides the path calculated using 'prefix')   364           * web_app_dir       - the directory where Web applications and scripts   365                                 reside (such as /home/www-user/cgi-bin)   366           * web_static_dir    - optional: the directory where static Web   367                                 resources reside (such as /home/www-user/htdocs)   368           * web_site_dir      - optional: the directory where Web site   369                                 definitions reside (such as   370                                 /etc/apache2/sites-available)   371    372         The following site settings are also applicable:   373    374           * common_dir        - the directory where the Wiki configuration,   375                                 resources and instance will reside (such as   376                                 /home/www-user/mywiki)   377           * farm_config       - optional: any Wiki farm configuration file for   378                                 multiple Wiki deployments (overrides the   379                                 'common_dir' setting)   380           * site_config       - optional: a specific configuration file location   381                                 (overrides the 'common_dir' setting)   382           * url_path          - the URL path at which the Wiki will be made   383                                 available (such as / or /mywiki)   384           * superuser         - the name of the site's superuser (such as   385                                 "AdminUser")   386           * site_name         - the name of the site (such as "My Wiki")   387           * site_identifier   - optional: an identifier used to refer to the   388                                 site, typically derived from 'site_name'   389           * front_page_name   - the front page name for the site (such as   390                                 "FrontPage" or a specific name for the site)   391           * theme_default     - optional: the default theme (such as modern)   392         """   393    394         self.moin_distribution = moin_distribution   395         self.superuser = superuser   396         self.site_name = site_name   397         self.site_identifier = site_identifier or site_name.replace(" ", "").lower()   398         self.front_page_name = front_page_name   399         self.farm_config = farm_config   400         self.site_config = site_config   401         self.theme_default = theme_default   402    403         # NOTE: Support the detection of the Apache sites directory.   404    405         self.prefix, self.site_packages, self.web_app_dir, self.web_site_dir, self.web_static_dir, self.common_dir = \   406             map(self._get_abspath, (prefix, site_packages, web_app_dir, web_site_dir, web_static_dir, common_dir))   407    408         if not self.web_app_dir:   409             raise TypeError, "The 'web_app_dir' setting must be specified."   410    411         # Strip any trailing "/" from the URL path.   412    413         if not url_path:   414             raise TypeError, "The 'url_path' setting must be specified."   415    416         if url_path != "/" and url_path.endswith("/"):   417             self.url_path = url_path[:-1]   418         else:   419             self.url_path = url_path   420    421         # Define and create specific directories.   422         # Here are the configuration and actual Wiki data directories.   423    424         if not self.common_dir:   425             raise TypeError, "The 'common_dir' setting must be specified."   426    427         # Define the place where the MoinMoin package will actually reside.   428    429         if not self.prefix and not self.site_packages:   430             raise TypeError, "Either the 'prefix' or the 'site_packages' setting must be specified."   431    432         self.prefix_site_packages = self.site_packages or \   433             join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages")   434    435         # Find the version.   436    437         self.moin_version = self.get_moin_version()   438    439         # The static resources reside in different locations depending on the   440         # version of MoinMoin, but the Web server is used to serve static   441         # resources in both cases, even though MoinMoin 1.9 can serve static   442         # files itself.   443    444         # A shared data directory may be in use.   445    446         self.htdocs_dir_source = join(self.get_moin_data(), "htdocs")   447    448         if self.htdocs_dir_source is None or not exists(self.htdocs_dir_source):   449    450             # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs   451    452             if self.moin_version.startswith("1.9"):   453                 self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs")   454             else:   455                 raise SetupException, "The static resources could not be found."   456    457         # Add the static identifier to the URL path. For example:   458         # /         -> /moin_static187   459         # /hgwiki   -> /hgwiki-moin_static187   460    461         self.static_url_path = self.url_path + (self.url_path != "/" and "-" or "") + self.get_static_identifier()   462    463         # In limited hosting, the static resources directory is related to   464         # the URL path.   465    466         if self.limited_hosting():   467             self.htdocs_dir = join(self.web_static_dir or self.web_app_dir, self.static_url_path.lstrip("/"))   468    469         # Otherwise, a mapping is made to the directory.   470         # This may be placed in a special static directory if desired.   471    472         else:   473             self.htdocs_dir = join(self.web_static_dir or self.common_dir, "htdocs")   474    475     def show_config(self):   476    477         "Show the configuration."   478    479         print   480         for section in ("source", "instance", "site"):   481             print section.title()   482             print "-" * len(section)   483             print   484             for setting in getattr(self, "%s_config_names" % section):   485                 print "%-20s%s" % (setting, getattr(self, setting))   486             print   487    488     def _get_abspath(self, d):   489         return d and abspath(d) or None   490    491     def get_moin_version(self):   492    493         "Return the MoinMoin version."   494    495         if self.moin_distribution:   496             this_dir = os.getcwd()   497             try:   498                 os.chdir(self.moin_distribution)   499                 version = self.get_moin_version_from_package_info() or \   500                     self.get_moin_version_from_import()   501                 if version:   502                     return version   503    504             finally:   505                 os.chdir(this_dir)   506    507         else:   508             return self.get_moin_version_from_import()   509    510     def get_moin_version_from_package_info(self):   511    512         "Inspect the MoinMoin package information, returning the version."   513    514         try:   515             f = open("PKG-INFO")   516             try:   517                 for line in f.xreadlines():   518                     columns = line.split()   519                     if columns[0] == "Version:":   520                         return columns[1]   521             finally:   522                 f.close()   523    524         except IOError:   525             pass   526    527         return None   528    529     def get_moin_version_from_import(self):   530    531         "Return the MoinMoin version from an import of the package itself."   532    533         # Where no distribution information can be read, try and import an   534         # installed version module.   535    536         f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable)   537         try:   538             return f.read().strip()   539         finally:   540             f.close()   541    542     def get_moin_data(self):   543    544         "Return the exact location of MoinMoin data."   545    546         return self.moin_distribution and join(self.moin_distribution, "wiki") or \   547             self.prefix and join(self.prefix, "share", "moin") or None   548    549     def get_wikiconfig_directory(self):   550    551         "Return the location of the Wiki configuration."   552    553         if self.site_config:   554             return split(self.site_config)[0]   555         else:   556             return self.common_dir   557    558     def get_static_identifier(self):   559    560         "Return the static URL/directory identifier for the Wiki."   561    562         return "moin_static%s" % self.moin_version.replace(".", "")   563    564     def get_plugin_directory(self, plugin_type):   565    566         "Return the directory for plugins of the given 'plugin_type'."   567    568         data_dir = join(self.common_dir, "data")   569         return join(data_dir, "plugin", plugin_type)   570    571     def limited_hosting(self):   572    573         "Return whether limited Web hosting is being used."   574    575         return not self.web_site_dir   576    577     def ensure_directories(self):   578    579         "Make sure that all the directories are available."   580    581         for d in (self.common_dir, self.web_app_dir, self.web_static_dir, self.web_site_dir):   582             if d is not None and not exists(d):   583                 os.makedirs(d)   584    585     def get_theme_directories(self, theme_name=None):   586    587         """   588         Return tuples of the form (theme name, theme directory) for all themes,   589         or for a single theme if the optional 'theme_name' is specified.   590         """   591    592         filenames = theme_name and [theme_name] or os.listdir(self.htdocs_dir)   593         directories = []   594    595         for filename in filenames:   596             theme_dir = join(self.htdocs_dir, filename)   597    598             if not exists(theme_dir) or not isdir(theme_dir):   599                 continue   600    601             directories.append((filename, theme_dir))   602    603         return directories   604    605     # Main methods.   606    607     def setup(self):   608    609         "Set up the installation."   610    611         self.ensure_directories()   612         self.install_moin()   613         self.edit_moin_script()   614         self._setup_wiki()   615    616     def setup_wiki(self):   617    618         "Set up a Wiki without installing MoinMoin."   619    620         self.ensure_directories()   621         self._setup_wiki()   622    623     def _setup_wiki(self):   624    625         "Set up a Wiki without installing MoinMoin."   626    627         self.install_data()   628         self.install_static_data()   629         self.configure_moin()   630         self.add_superuser()   631         self.edit_moin_web_script(self.make_site_files())   632         self.make_post_install_script()   633    634         if self.moin_version.startswith("1.9"):   635             note("You may need to visit the LanguageSetup page in the Wiki to create the standard set of pages.")   636    637     def install_moin(self):   638    639         "Enter the distribution directory and run the setup script."   640    641         # NOTE: Possibly check for an existing installation and skip repeated   642         # NOTE: installation attempts.   643    644         if not self.moin_distribution:   645             raise SetupException, "Cannot install MoinMoin without a 'moin_distribution' setting being defined."   646    647         this_dir = os.getcwd()   648         os.chdir(self.moin_distribution)   649    650         log_filename = "install-%s.log" % split(self.common_dir)[-1]   651    652         status("Installing MoinMoin in %s..." % self.prefix)   653    654         install_cmd = "install"   655         options = "--prefix='%s' --record='%s'" % (self.prefix, log_filename)   656    657         os.system("python setup.py --quiet %s %s --force" % (install_cmd, options))   658    659         os.chdir(this_dir)   660    661     def install_data(self):   662    663         "Install Wiki data into an instance."   664    665         moin_data = self.get_moin_data()   666    667         if not moin_data:   668             raise SetupException, \   669                 "Cannot install MoinMoin data without either a 'moin_distribution' or a 'prefix' setting being defined."   670    671         # The default wikiconfig assumes data and underlay in the same directory.   672    673         status("Installing data and underlay in %s..." % self.common_dir)   674    675         for d in ("data", "underlay"):   676             source = join(moin_data, d)   677             source_tar = source + os.path.extsep + "tar"   678    679             if os.path.exists(source):   680                 shutil.copytree(source, join(self.common_dir, d))   681             elif os.path.exists(source_tar):   682    683                 note("Copying archive %s instead of directory %s. Running...\n"   684                     "make pagepacks\n"   685                     "in the distribution directory should rectify this." % (source_tar, source))   686    687                 shutil.copy(source_tar, self.common_dir)   688                 os.system("tar xf %s -C %s" % (source_tar, self.common_dir))   689             else:   690                 status("Could not copy %s into installed Wiki." % d)   691    692     def install_static_data(self):   693    694         "Install static Web data if appropriate."   695    696         if not exists(self.htdocs_dir):   697             os.mkdir(self.htdocs_dir)   698    699         for item in os.listdir(self.htdocs_dir_source):   700             path = join(self.htdocs_dir_source, item)   701             if isdir(path):   702                 shutil.copytree(path, join(self.htdocs_dir, item))   703             else:   704                 shutil.copy(path, join(self.htdocs_dir, item))   705    706     def configure_moin(self):   707    708         "Edit the Wiki configuration file."   709    710         moin_data = self.get_moin_data()   711    712         if not moin_data:   713             raise SetupException, \   714                 "Cannot configure MoinMoin without either a 'moin_distribution' or a 'prefix' setting being defined."   715    716         # NOTE: Single Wiki only so far.   717    718         # NOTE: MoinMoin usually uses an apparently common URL space associated   719         # NOTE: with the version, but more specific locations are probably   720         # NOTE: acceptable if less efficient.   721    722         url_prefix_static = "%r" % self.static_url_path   723    724         # Use a farm configuration file.   725    726         if self.farm_config:   727             wikiconfig_py = self.farm_config   728    729         # Or copy the Wiki configuration file from the distribution.   730    731         else:   732             wikiconfig_py = join(self.common_dir, "wikiconfig.py")   733             shutil.copyfile(join(moin_data, "config", "wikiconfig.py"), wikiconfig_py)   734    735         status("Editing configuration from %s..." % wikiconfig_py)   736    737         # Edit the Wiki configuration file.   738    739         wikiconfig = Configuration(wikiconfig_py)   740    741         try:   742             wikiconfig.set("url_prefix_static", url_prefix_static, raw=1)   743             wikiconfig.set("superuser", [self.superuser])   744             wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser)   745    746             # Edit any created Wiki configuration.   747    748             if not self.site_config:   749                 self._configure_moin(wikiconfig)   750    751         finally:   752             wikiconfig.close()   753    754         # Edit any separate site configuration file.   755    756         if self.site_config:   757             status("Editing configuration from %s..." % self.site_config)   758    759             wikiconfig = Configuration(self.site_config)   760    761             try:   762                 self._configure_moin(wikiconfig)   763             finally:   764                 wikiconfig.close()   765    766     def _configure_moin(self, wikiconfig):   767    768         """   769         Configure Moin, accessing the configuration file using 'wikiconfig'.   770         """   771    772         # Specific site configurations also appear to need 'data_dir', even in   773         # 1.9.   774    775         if not self.moin_version.startswith("1.9") or self.site_config:   776             data_dir = join(self.common_dir, "data")   777             data_underlay_dir = join(self.common_dir, "underlay")   778    779             wikiconfig.set("data_dir", data_dir)   780             wikiconfig.set("data_underlay_dir", data_underlay_dir)   781    782         wikiconfig.set("site_name", self.site_name)   783         wikiconfig.set("page_front_page", self.front_page_name, count=1)   784    785         if self.theme_default is not None:   786             wikiconfig.set("theme_default", self.theme_default)   787    788     def edit_moin_script(self):   789    790         "Edit the moin script."   791    792         moin_script = join(self.prefix, "bin", "moin")   793    794         status("Editing moin script at %s..." % moin_script)   795    796         s = readfile(moin_script)   797         s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages)   798    799         writefile(moin_script, s)   800    801     def edit_moin_web_script(self, site_file_configured=1):   802    803         "Edit and install CGI script."   804    805         # NOTE: CGI only so far.   806         # NOTE: Permissions should be checked.   807    808         moin_data = self.get_moin_data()   809    810         if self.moin_version.startswith("1.9"):   811             moin_cgi_script = "moin.fcgi"   812         else:   813             moin_cgi_script = "moin.cgi"   814    815         moin_cgi = join(moin_data, "server", moin_cgi_script)   816         moin_cgi_installed = join(self.web_app_dir, "moin.cgi")   817    818         status("Editing moin.cgi script from %s, writing to %s..." % (moin_cgi, moin_cgi_installed))   819    820         s = readfile(moin_cgi)   821         s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s)   822         s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.common_dir, s)   823    824         # Handle differences in script names when using limited hosting with   825         # URL rewriting.   826    827         if self.limited_hosting():   828             if not site_file_configured:   829                 note("Site file not configured: script name not changed.")   830             else:   831                 if self.moin_version.startswith("1.9"):   832                     s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s)   833                 else:   834                     s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s)   835    836         # NOTE: Use CGI for now.   837    838         if self.moin_version.startswith("1.9"):   839             s = moin_cgi_force_cgi.sub(r"\1", s)   840    841         writefile(moin_cgi_installed, s)   842         os.system("chmod a+rx '%s'" % moin_cgi_installed)   843    844         # Fix the cause of opaque errors in some Apache environments.   845    846         os.system("chmod go-w '%s'" % moin_cgi_installed)   847    848     def add_superuser(self):   849    850         "Add the superuser account."   851    852         moin_script = join(self.prefix, "bin", "moin")   853    854         print "Creating superuser", self.superuser, "using..."   855         email = raw_input("E-mail address: ")   856         password = getpass("Password: ")   857    858         path = os.environ.get("PYTHONPATH", "")   859    860         if path:   861             os.environ["PYTHONPATH"] = path + ":" + self.common_dir   862         else:   863             os.environ["PYTHONPATH"] = self.common_dir   864    865         cmd = moin_script + " --config-dir='%s' account create --name='%s' --email='%s' --password='%s'" % (   866             self.common_dir, self.superuser, email, password)   867         os.system(cmd)   868    869         if path:   870             os.environ["PYTHONPATH"] = path   871         else:   872             del os.environ["PYTHONPATH"]   873    874     def make_site_files(self):   875    876         "Make the Apache site files."   877    878         # NOTE: Using local namespace for substitution.   879    880         # Where the site definitions and applications directories are different,   881         # use a normal site definition.   882    883         if not self.limited_hosting():   884    885             site_def = join(self.web_site_dir, self.site_identifier)   886    887             s = apache_site % self.__dict__   888             s += apache_site_extra % self.__dict__   889    890             status("Writing Apache site definitions to %s..." % site_def)   891             writefile(site_def, s)   892    893             note("Copy the site definitions to the appropriate sites directory if appropriate.\n"   894                 "Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).")   895    896             return 1   897    898         # Otherwise, use an .htaccess file.   899    900         else:   901             site_def = join(self.web_app_dir, ".htaccess")   902    903             s = apache_htaccess_combined_mod_rewrite % self.__dict__   904    905             status("Writing .htaccess file to %s..." % site_def)   906             try:   907                 writefile(site_def, s)   908             except IOError:   909                 note("The .htaccess file could not be written. This will also affect the script name setting.")   910                 return 0   911             else:   912                 return 1   913    914     def make_post_install_script(self):   915    916         "Write a post-install script with additional actions."   917    918         # Work out whether setfacl works.   919    920         fd, temp_filename = tempfile.mkstemp(dir=self.common_dir)   921         os.close(fd)   922    923         have_setfacl = os.system("setfacl -m user:%(web_user)s:r %(file)s > /dev/null 2>&1" % {   924             "web_user" : self.web_user, "file" : temp_filename}) == 0   925    926         os.remove(temp_filename)   927    928         # Create the scripts.   929    930         this_user = os.environ["USER"]   931         postinst_scripts = "moinsetup-post-chown.sh", "moinsetup-post-setfacl.sh"   932    933         vars = {}   934         vars.update(Installation.__dict__)   935         vars.update(self.__dict__)   936         vars.update(locals())   937    938         for postinst_script, start, extra in [   939             (postinst_scripts[0], postsetup_chown_chmod, postsetup_chown_extra),   940             (postinst_scripts[1], postsetup_setfacl, postsetup_setfacl_extra)   941             ]:   942    943             s = start % vars   944             s += extra % vars   945    946             writefile(postinst_script, s)   947             os.chmod(postinst_script, 0755)   948    949         if have_setfacl:   950             note("Run %s to set file ownership and permissions.\n"   951                 "If this somehow fails..." % postinst_scripts[1])   952    953         note("Run %s as root to set file ownership and permissions." % postinst_scripts[0])   954    955     # Accessory methods.   956    957     def reconfigure_moin(self, name=None, value=None, raw=0):   958    959         """   960         Edit the installed Wiki configuration file, setting a parameter with any   961         given 'name' to the given 'value', treating the value as a raw   962         expression (not a string) if 'raw' is set to a true value.   963    964         If 'name' and the remaining parameters are omitted, the default   965         configuration activity is performed.   966    967         If the 'site_config' setting is defined, the specific site configuration   968         will be changed.   969         """   970    971         if self.site_config:   972             wikiconfig_py = self.site_config   973         else:   974             wikiconfig_py = join(self.common_dir, "wikiconfig.py")   975    976         status("Editing configuration from %s..." % wikiconfig_py)   977    978         wikiconfig = Configuration(wikiconfig_py)   979    980         try:   981             # Perform default configuration.   982    983             if name is None and value is None:   984                 self._configure_moin(wikiconfig)   985             else:   986                 wikiconfig.set(name, value, raw=raw)   987    988         finally:   989             wikiconfig.close()   990    991     def set_auth_method(self, method_name):   992    993         """   994         Edit the installed Wiki configuration file, configuring the   995         authentication method having the given 'method_name'.   996    997         If the 'farm_config' setting is defined, the Wiki farm configuration   998         will be changed.   999         """  1000   1001         if self.farm_config:  1002             wikiconfig_py = self.farm_config  1003         else:  1004             wikiconfig_py = join(self.common_dir, "wikiconfig.py")  1005   1006         status("Editing configuration from %s..." % wikiconfig_py)  1007   1008         wikiconfig = Configuration(wikiconfig_py)  1009   1010         try:  1011             # OpenID authentication.  1012   1013             if method_name.lower() == "openid":  1014                 wikiconfig.set_import("MoinMoin.auth.openidrp", ["OpenIDAuth"])  1015   1016                 if self.moin_version.startswith("1.9"):  1017                     if wikiconfig.get("cookie_lifetime"):  1018                         wikiconfig.replace("cookie_lifetime", "(12, 12)", raw=1)  1019                     else:  1020                         wikiconfig.set("cookie_lifetime", "(12, 12)", raw=1)  1021                 else:  1022                     if wikiconfig.get("anonymous_session_lifetime"):  1023                         wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1)  1024                     else:  1025                         wikiconfig.set("anonymous_session_lifetime", "1000", raw=1)  1026   1027                 auth_object = "OpenIDAuth()"  1028   1029             # Default Moin authentication.  1030   1031             elif method_name.lower() in ("moin", "default"):  1032                 wikiconfig.set_import("MoinMoin.auth", ["MoinAuth"])  1033                 auth_object = "MoinAuth()"  1034   1035             # REMOTE_USER authentication.  1036   1037             elif method_name.lower() in ("given", "remote-user"):  1038                 wikiconfig.set_import("MoinMoin.auth.http", ["HTTPAuth"])  1039                 auth_object = "HTTPAuth(autocreate=True)"  1040   1041             # Other methods are not currently supported.  1042   1043             else:  1044                 return  1045   1046             # Edit the authentication setting.  1047   1048             auth = wikiconfig.get("auth")  1049             if auth:  1050                 wikiconfig.replace("auth", "%s + [%s]" % (auth, auth_object), raw=1)  1051             else:  1052                 wikiconfig.set("auth", "[%s]" % auth_object, raw=1)  1053   1054         finally:  1055             wikiconfig.close()  1056   1057     def install_theme(self, theme_dir, theme_name=None):  1058   1059         """  1060         Install Wiki theme provided in the given 'theme_dir' having the given  1061         optional 'theme_name' (if different from the 'theme_dir' name).  1062         """  1063   1064         theme_dir = normpath(theme_dir)  1065         theme_name = theme_name or split(theme_dir)[-1]  1066         theme_module = join(theme_dir, theme_name + extsep + "py")  1067   1068         plugin_theme_dir = self.get_plugin_directory("theme")  1069   1070         # Copy the theme module.  1071   1072         status("Copying theme module to %s..." % plugin_theme_dir)  1073   1074         shutil.copy(theme_module, plugin_theme_dir)  1075   1076         # Copy the resources.  1077   1078         resources_dir = join(self.htdocs_dir, theme_name)  1079   1080         if not exists(resources_dir):  1081             os.mkdir(resources_dir)  1082   1083         status("Copying theme resources to %s..." % resources_dir)  1084   1085         for d in ("css", "img"):  1086             target_dir = join(resources_dir, d)  1087             if exists(target_dir):  1088                 status("Replacing %s..." % target_dir)  1089                 shutil.rmtree(target_dir)  1090             shutil.copytree(join(theme_dir, d), target_dir)  1091   1092         # Copy additional resources from other themes.  1093   1094         resources_source_dir = join(self.htdocs_dir, self.theme_master)  1095         target_dir = join(resources_dir, "css")  1096   1097         status("Copying resources from %s..." % resources_source_dir)  1098   1099         for css_file in self.extra_theme_css_files:  1100             css_file_path = join(resources_source_dir, "css", css_file)  1101             if exists(css_file_path):  1102                 shutil.copy(css_file_path, target_dir)  1103   1104         note("Don't forget to add theme resources for extensions for this theme.\n"  1105             "Don't forget to edit this theme's stylesheets for extensions.")  1106   1107     def install_extension_package(self, extension_dir):  1108   1109         "Install any libraries from 'extension_dir' using a setup script."  1110   1111         this_dir = os.getcwd()  1112         os.chdir(extension_dir)  1113   1114         options = "install --install-lib=%s" % self.prefix_site_packages  1115   1116         os.system("python setup.py %s" % options)  1117         os.chdir(this_dir)  1118   1119     def install_plugins(self, plugins_dir, plugin_type):  1120   1121         """  1122         Install Wiki actions provided in the given 'plugins_dir' of the  1123         specified 'plugin_type'.  1124         """  1125   1126         plugin_target_dir = self.get_plugin_directory(plugin_type)  1127   1128         # Copy the modules.  1129   1130         status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir))  1131   1132         for module in glob(join(plugins_dir, "*%spy" % extsep)):  1133             shutil.copy(module, plugin_target_dir)  1134   1135     def install_actions(self, actions_dir):  1136   1137         "Install Wiki actions provided in the given 'actions_dir'."  1138   1139         self.install_plugins(actions_dir, "action")  1140   1141     def install_macros(self, macros_dir):  1142   1143         "Install Wiki macros provided in the given 'macros_dir'."  1144   1145         self.install_plugins(macros_dir, "macro")  1146   1147     def install_parsers(self, parsers_dir):  1148   1149         "Install Wiki macros provided in the given 'parsers_dir'."  1150   1151         self.install_plugins(parsers_dir, "parser")  1152   1153     def install_theme_resources(self, theme_resources_dir, theme_name=None):  1154   1155         """  1156         Install theme resources provided in the given 'theme_resources_dir'. If  1157         a specific 'theme_name' is given, only that theme will be given the  1158         specified resources.  1159         """  1160   1161         for theme_name, theme_dir in self.get_theme_directories(theme_name):  1162   1163             # Copy the resources.  1164   1165             copied = 0  1166   1167             for d in ("css", "img"):  1168                 source_dir = join(theme_resources_dir, d)  1169                 target_dir = join(theme_dir, d)  1170   1171                 if not exists(target_dir):  1172                     continue  1173   1174                 for resource in glob(join(source_dir, "*%s*" % extsep)):  1175                     shutil.copy(resource, target_dir)  1176                     copied = 1  1177   1178             if copied:  1179                 status("Copied theme resources into %s..." % theme_dir)  1180   1181         note("Don't forget to edit theme stylesheets for any extensions.")  1182   1183     def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None):  1184   1185         """  1186         Edit the given 'theme_stylesheet', ensuring (or removing) a reference to  1187         the 'imported_stylesheet' according to the given 'action' (optional,  1188         defaulting to "ensure"). If a specific 'theme_name' is given, only that  1189         theme will be affected.  1190         """  1191   1192         if action == "ensure":  1193             ensure = 1  1194         elif action == "remove":  1195             ensure = 0  1196         else:  1197             error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action)  1198             return  1199   1200         for theme_name, theme_dir in self.get_theme_directories(theme_name):  1201   1202             # Locate the resources.  1203   1204             css_dir = join(theme_dir, "css")  1205   1206             if not exists(css_dir):  1207                 continue  1208   1209             theme_stylesheet_filename = join(css_dir, theme_stylesheet)  1210             imported_stylesheet_filename = join(css_dir, imported_stylesheet)  1211   1212             if not exists(theme_stylesheet_filename):  1213                 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name))  1214                 continue  1215   1216             if not exists(imported_stylesheet_filename):  1217                 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name))  1218                 continue  1219   1220             # Edit the resources.  1221   1222             s = readfile(theme_stylesheet_filename)  1223             after_point = 0  1224   1225             for stylesheet_import in css_import_stylesheet.finditer(s):  1226                 before, filename, after = stylesheet_import.groups()  1227                 before_point, after_point = stylesheet_import.span()  1228   1229                 # Test the import for a reference to the requested imported  1230                 # stylesheet.  1231   1232                 if filename == imported_stylesheet:  1233                     if ensure:  1234                         break  1235                     else:  1236                         if s[after_point:after_point+1] == "\n":  1237                             after_point += 1  1238                         s = "%s%s" % (s[:before_point], s[after_point:])  1239   1240                         status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))  1241                         writefile(theme_stylesheet_filename, s)  1242                         break  1243   1244             # Where no import references the imported stylesheet, insert a  1245             # reference into the theme stylesheet.  1246   1247             else:  1248                 if ensure:  1249   1250                     # Assume that the stylesheet can follow other imports.  1251   1252                     if s[after_point:after_point+1] == "\n":  1253                         after_point += 1  1254                     s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:])  1255   1256                     status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))  1257                     writefile(theme_stylesheet_filename, s)  1258   1259     def make_page_package(self, page_directory, package_filename):  1260   1261         """  1262         Make a package containing the pages in 'page_directory', using the  1263         filenames as the page names, and writing the package to a file with the  1264         given 'package_filename'.  1265         """  1266   1267         package = ZipFile(package_filename, "w")  1268   1269         try:  1270             script = ["MoinMoinPackage|1"]  1271   1272             for filename in os.listdir(page_directory):  1273                 pathname = join(page_directory, filename)  1274   1275                 # Add files as pages having the filename as page name.  1276   1277                 if os.path.isfile(pathname):  1278                     package.write(pathname, filename)  1279                     script.append("AddRevision|%s|%s" % (filename, filename))  1280   1281                 # Add directories ending with "-attachments" as collections of  1282                 # attachments for a particular page.  1283   1284                 elif os.path.isdir(pathname) and filename.endswith("-attachments"):  1285                     parent = filename[:-len("-attachments")]  1286   1287                     # Add each file as an attachment.  1288   1289                     for attachment in os.listdir(pathname):  1290                         zipname = "%s_%s" % (filename, attachment)  1291                         package.write(join(pathname, attachment), zipname)  1292                         script.append("AddAttachment|%s|%s|%s||" % (zipname, attachment, parent))  1293   1294             package.writestr("MOIN_PACKAGE", "\n".join(script))  1295   1296         finally:  1297             package.close()  1298   1299     def install_page_package(self, package_filename):  1300   1301         """  1302         Install a package from the file with the given 'package_filename'.  1303         """  1304   1305         path = os.environ.get("PYTHONPATH", "")  1306   1307         conf_dir = self.get_wikiconfig_directory()  1308   1309         if path:  1310             os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + conf_dir  1311         else:  1312             os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + conf_dir  1313   1314         installer = join(self.prefix_site_packages, "MoinMoin", "packages.py")  1315         cmd = "python %s i %s" % (installer, package_filename)  1316         os.system(cmd)  1317   1318         if path:  1319             os.environ["PYTHONPATH"] = path  1320         else:  1321             del os.environ["PYTHONPATH"]  1322   1323 def show_methods():  1324     print "Methods:"  1325     print  1326     for method_name in Installation.method_names:  1327         doc = getattr(Installation, method_name).__doc__.strip()  1328         print "%-30s%-s" % (method_name, format(doc, 30))  1329     print  1330   1331 # Command line option syntax.  1332   1333 syntax_description = "[ -f <config-filename> ] ( -m <method> | --method=METHOD ) [ <method-argument> ... ]"  1334   1335 # Main program.  1336   1337 if __name__ == "__main__":  1338     from ConfigParser import ConfigParser  1339     import sys, cmdsyntax  1340   1341     # Check the command syntax.  1342   1343     syntax = cmdsyntax.Syntax(syntax_description)  1344     try:  1345         matches = syntax.get_args(sys.argv[1:])  1346         args = matches[0]  1347     except IndexError:  1348         print "Syntax:"  1349         print sys.argv[0], syntax_description  1350         print  1351         show_methods()  1352         sys.exit(1)  1353   1354     # Obtain configuration details.  1355   1356     try:  1357         config_filename = args.get("config-filename", "moinsetup.cfg")  1358   1359         if not exists(config_filename):  1360             print "Configuration", config_filename, "not found."  1361             sys.exit(1)  1362   1363         config = ConfigParser()  1364         config.read(config_filename)  1365   1366         # Obtain as many arguments as needed from the configuration.  1367   1368         config_arguments = dict(config.items("installation") + config.items("site"))  1369         method_arguments = args.get("method-argument", [])  1370   1371         # Attempt to initialise the configuration.  1372   1373         installation = Installation(**config_arguments)  1374   1375     except TypeError, exc:  1376         print "Error:"  1377         print  1378         print exc.args[0]  1379         print  1380         print "Configuration settings:"  1381         print  1382         print Installation.__init__.__doc__  1383         print  1384         sys.exit(1)  1385   1386     # Obtain the method.  1387   1388     try:  1389         method = getattr(installation, args["method"])  1390     except AttributeError:  1391         show_methods()  1392         sys.exit(1)  1393   1394     try:  1395         method(*method_arguments)  1396     except TypeError, exc:  1397         print "Error:"  1398         print  1399         print exc.args[0]  1400         print  1401         print "Method documentation:"  1402         print  1403         print method.__doc__  1404         print  1405         sys.exit(1)  1406   1407 # vim: tabstop=4 expandtab shiftwidth=4