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