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.1" 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, web_site_dir, 250 common_dir, url_path, superuser, site_name, front_page_name, 251 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 * web_site_dir - the directory where Web site definitions reside 262 (such as /etc/apache2/sites-available) 263 * common_dir - the directory where the Wiki configuration, 264 resources and instance will reside (such as 265 /home/www-user/mywiki) 266 * url_path - the URL path at which the Wiki will be made 267 available (such as / or /mywiki) 268 * superuser - the name of the site's superuser (such as 269 "AdminUser") 270 * site_name - the name of the site (such as "My Wiki") 271 * front_page_name - the front page name for the site (such as 272 "FrontPage" or a specific name for the site) 273 * theme_default - optional: the default theme (such as modern) 274 """ 275 276 self.moin_distribution = moin_distribution 277 self.superuser = superuser 278 self.site_name = site_name 279 self.front_page_name = front_page_name 280 self.theme_default = theme_default 281 282 # NOTE: Support the detection of the Apache sites directory. 283 284 self.prefix, self.web_app_dir, self.web_site_dir, self.common_dir = \ 285 map(abspath, (prefix, web_app_dir, web_site_dir, common_dir)) 286 287 # Strip any trailing "/" from the URL path. 288 289 if url_path != "/" and url_path.endswith("/"): 290 self.url_path = url_path[:-1] 291 else: 292 self.url_path = url_path 293 294 # Define and create specific directories. 295 # Here are the configuration and actual Wiki data directories. 296 297 self.conf_dir = join(self.common_dir, "conf") 298 self.instance_dir = join(self.common_dir, "wikidata") 299 300 # Define the place where the MoinMoin package will actually reside. 301 302 self.prefix_site_packages = join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages") 303 304 # Find the version. 305 306 self.moin_version = self.get_moin_version() 307 308 # The static resources reside in different locations depending on the 309 # version of MoinMoin. Moreover, these resources may end up in a 310 # published directory for 1.8 installations where the Web server cannot 311 # be instructed to fetch the content from outside certain designated 312 # locations. 313 314 # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs 315 316 if self.moin_version.startswith("1.9"): 317 self.htdocs_dir = self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs") 318 319 # 1.8: moin/share/moin/htdocs (optionally copied to a Web directory) 320 321 else: 322 self.htdocs_dir_source = join(self.instance_dir, "share", "moin", "htdocs") 323 324 if self.limited_hosting(): 325 self.htdocs_dir = join(self.web_app_dir, self.get_static_identifier()) 326 else: 327 self.htdocs_dir = self.htdocs_dir_source 328 329 def get_moin_version(self): 330 331 "Inspect the MoinMoin package information, returning the version." 332 333 this_dir = os.getcwd() 334 os.chdir(self.moin_distribution) 335 336 try: 337 try: 338 f = open("PKG-INFO") 339 try: 340 for line in f.xreadlines(): 341 columns = line.split() 342 if columns[0] == "Version:": 343 return columns[1] 344 345 return None 346 347 finally: 348 f.close() 349 350 except IOError: 351 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable) 352 try: 353 return f.read().strip() 354 finally: 355 f.close() 356 finally: 357 os.chdir(this_dir) 358 359 def get_static_identifier(self): 360 361 "Return the static URL/directory identifier for the Wiki." 362 363 return "moin_static%s" % self.moin_version.replace(".", "") 364 365 def get_plugin_directory(self, plugin_type): 366 367 "Return the directory for plugins of the given 'plugin_type'." 368 369 data_dir = join(self.conf_dir, "data") 370 return join(data_dir, "plugin", plugin_type) 371 372 def limited_hosting(self): 373 374 "Return whether limited Web hosting is being used." 375 376 return self.web_site_dir == self.web_app_dir 377 378 def ensure_directories(self): 379 380 "Make sure that all the directories are available." 381 382 for d in (self.conf_dir, self.instance_dir, self.web_app_dir, self.web_site_dir): 383 if not exists(d): 384 os.makedirs(d) 385 386 def get_theme_directories(self, theme_name=None): 387 388 """ 389 Return tuples of the form (theme name, theme directory) for all themes, 390 or for a single theme if the optional 'theme_name' is specified. 391 """ 392 393 filenames = theme_name and [theme_name] or os.listdir(self.htdocs_dir) 394 directories = [] 395 396 for filename in filenames: 397 theme_dir = join(self.htdocs_dir, filename) 398 399 if not exists(theme_dir) or not isdir(theme_dir): 400 continue 401 402 directories.append((filename, theme_dir)) 403 404 return directories 405 406 # Main methods. 407 408 def setup(self): 409 410 "Set up the installation." 411 412 self.ensure_directories() 413 self.install_moin() 414 self._setup_wiki() 415 416 def setup_wiki(self): 417 418 "Set up a Wiki without installing MoinMoin." 419 420 self.ensure_directories() 421 self.install_moin(data_only=1) 422 self._setup_wiki() 423 424 def _setup_wiki(self): 425 426 "Set up a Wiki without installing MoinMoin." 427 428 self.install_data() 429 self.configure_moin() 430 self.edit_moin_script() 431 self.edit_moin_web_script() 432 self.add_superuser() 433 self.make_site_files() 434 self.make_post_install_script() 435 436 def install_moin(self, data_only=0): 437 438 "Enter the distribution directory and run the setup script." 439 440 # NOTE: Possibly check for an existing installation and skip repeated 441 # NOTE: installation attempts. 442 443 this_dir = os.getcwd() 444 os.chdir(self.moin_distribution) 445 446 log_filename = "install-%s.log" % split(self.common_dir)[-1] 447 448 status("Installing MoinMoin%s in %s..." % (data_only and " (data only)" or "", self.prefix)) 449 450 if data_only: 451 install_cmd = "install_data" 452 options = "--install-dir='%s'" % self.instance_dir 453 else: 454 install_cmd = "install" 455 options = "--prefix='%s' --install-data='%s' --record='%s'" % (self.prefix, self.instance_dir, log_filename) 456 457 os.system("python setup.py --quiet %s %s --force" % (install_cmd, options)) 458 459 os.chdir(this_dir) 460 461 def install_data(self): 462 463 "Install Wiki data." 464 465 # The default wikiconfig assumes data and underlay in the same directory. 466 467 status("Installing data and underlay in %s..." % self.conf_dir) 468 469 for d in ("data", "underlay"): 470 source = join(self.moin_distribution, "wiki", d) 471 source_tar = source + os.path.extsep + "tar" 472 d_tar = source + os.path.extsep + "tar" 473 474 if os.path.exists(source): 475 shutil.copytree(source, join(self.conf_dir, d)) 476 elif os.path.exists(source_tar): 477 shutil.copy(source_tar, self.conf_dir) 478 os.system("tar xf %s -C %s" % (d_tar, self.conf_dir)) 479 else: 480 status("Could not copy %s into installed Wiki." % d) 481 482 # Copy static Web data if appropriate. 483 484 if not self.moin_version.startswith("1.9") and self.limited_hosting(): 485 486 if not exists(self.htdocs_dir): 487 os.mkdir(self.htdocs_dir) 488 489 for item in os.listdir(self.htdocs_dir_source): 490 path = join(self.htdocs_dir_source, item) 491 if isdir(path): 492 shutil.copytree(path, join(self.htdocs_dir, item)) 493 else: 494 shutil.copy(path, join(self.htdocs_dir, item)) 495 496 def configure_moin(self): 497 498 "Edit the Wiki configuration file." 499 500 # NOTE: Single Wiki only so far. 501 502 # Static URLs seem to be different in MoinMoin 1.9.x. 503 # For earlier versions, reserve URL space alongside the Wiki. 504 # NOTE: MoinMoin usually uses an apparently common URL space associated 505 # NOTE: with the version, but more specific locations are probably 506 # NOTE: acceptable if less efficient. 507 508 if self.moin_version.startswith("1.9"): 509 self.static_url_path = self.url_path 510 url_prefix_static = "%r + url_prefix_static" % self.static_url_path 511 else: 512 # Add the static identifier to the URL path. For example: 513 # / -> /moin_static187 514 # /hgwiki -> /hgwiki/moin_static187 515 516 self.static_url_path = self.url_path + (self.url_path != "/" and "-" or "") + self.get_static_identifier() 517 url_prefix_static = "%r" % self.static_url_path 518 519 # Copy the Wiki configuration file from the distribution. 520 521 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 522 shutil.copyfile(join(self.moin_distribution, "wiki", "config", "wikiconfig.py"), wikiconfig_py) 523 524 status("Editing configuration from %s..." % wikiconfig_py) 525 526 # Edit the Wiki configuration file. 527 528 wikiconfig = Configuration(wikiconfig_py) 529 530 try: 531 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 532 wikiconfig.set("superuser", [self.superuser]) 533 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 534 535 if not self.moin_version.startswith("1.9"): 536 data_dir = join(self.conf_dir, "data") 537 data_underlay_dir = join(self.conf_dir, "underlay") 538 539 wikiconfig.set("data_dir", data_dir) 540 wikiconfig.set("data_underlay_dir", data_underlay_dir) 541 542 self._configure_moin(wikiconfig) 543 544 finally: 545 wikiconfig.close() 546 547 def _configure_moin(self, wikiconfig): 548 549 """ 550 Configure Moin, accessing the configuration file using 'wikiconfig'. 551 """ 552 553 wikiconfig.set("site_name", self.site_name) 554 wikiconfig.set("page_front_page", self.front_page_name, count=1) 555 556 if self.theme_default is not None: 557 wikiconfig.set("theme_default", self.theme_default) 558 559 def edit_moin_script(self): 560 561 "Edit the moin script." 562 563 moin_script = join(self.prefix, "bin", "moin") 564 565 status("Editing moin script at %s..." % moin_script) 566 567 s = readfile(moin_script) 568 s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages) 569 570 writefile(moin_script, s) 571 572 def edit_moin_web_script(self): 573 574 "Edit and install CGI script." 575 576 # NOTE: CGI only so far. 577 # NOTE: Permissions should be checked. 578 579 if self.moin_version.startswith("1.9"): 580 moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.fcgi") 581 else: 582 moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.cgi") 583 584 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 585 586 status("Editing moin.cgi script from %s..." % moin_cgi) 587 588 s = readfile(moin_cgi) 589 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s) 590 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.conf_dir, s) 591 592 # Handle differences in script names when using limited hosting with 593 # URL rewriting. 594 595 if self.limited_hosting(): 596 if self.moin_version.startswith("1.9"): 597 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s) 598 else: 599 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s) 600 601 # NOTE: Use CGI for now. 602 603 if self.moin_version.startswith("1.9"): 604 s = moin_cgi_force_cgi.sub(r"\1", s) 605 606 writefile(moin_cgi_installed, s) 607 os.system("chmod a+rx '%s'" % moin_cgi_installed) 608 609 def add_superuser(self): 610 611 "Add the superuser account." 612 613 moin_script = join(self.prefix, "bin", "moin") 614 615 print "Creating superuser", self.superuser, "using..." 616 email = raw_input("E-mail address: ") 617 password = getpass("Password: ") 618 619 path = os.environ.get("PYTHONPATH", "") 620 621 if path: 622 os.environ["PYTHONPATH"] = path + ":" + self.conf_dir 623 else: 624 os.environ["PYTHONPATH"] = self.conf_dir 625 626 os.system(moin_script + " account create --name='%s' --email='%s' --password='%s'" % (self.superuser, email, password)) 627 628 if path: 629 os.environ["PYTHONPATH"] = path 630 else: 631 del os.environ["PYTHONPATH"] 632 633 def make_site_files(self): 634 635 "Make the Apache site files." 636 637 # NOTE: Using local namespace for substitution. 638 639 # Where the site definitions and applications directories are different, 640 # use a normal site definition. 641 642 if not self.limited_hosting(): 643 644 site_def = join(self.web_site_dir, self.site_name) 645 646 s = apache_site % self.__dict__ 647 648 if not self.moin_version.startswith("1.9"): 649 s += apache_site_extra_moin18 % self.__dict__ 650 651 # Otherwise, use an .htaccess file. 652 653 else: 654 site_def = join(self.web_site_dir, ".htaccess") 655 656 s = apache_htaccess_combined_mod_rewrite % self.__dict__ 657 658 status("Writing Apache site definitions to %s..." % site_def) 659 writefile(site_def, s) 660 note("Copy the site definitions to the appropriate sites directory if appropriate.") 661 note("Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).") 662 663 def make_post_install_script(self): 664 665 "Write a post-install script with additional actions." 666 667 this_user = os.environ["USER"] 668 postinst_script = "moinsetup-post.sh" 669 670 s = "#!/bin/sh\n" 671 672 for d in ("data", "underlay"): 673 s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, join(self.conf_dir, d)) 674 s += "chmod -R g+w '%s'\n" % join(self.conf_dir, d) 675 676 if not self.moin_version.startswith("1.9"): 677 s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, self.htdocs_dir) 678 679 writefile(postinst_script, s) 680 os.chmod(postinst_script, 0755) 681 note("Run %s as root to set file ownership and permissions." % postinst_script) 682 683 # Accessory methods. 684 685 def reconfigure_moin(self, name=None, value=None, raw=0): 686 687 """ 688 Edit the installed Wiki configuration file, setting a parameter with any 689 given 'name' to the given 'value', treating the value as a raw 690 expression (not a string) if 'raw' is set to a true value. 691 692 If 'name' and the remaining parameters are omitted, the default 693 configuration activity is performed. 694 """ 695 696 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 697 698 status("Editing configuration from %s..." % wikiconfig_py) 699 700 wikiconfig = Configuration(wikiconfig_py) 701 702 try: 703 # Perform default configuration. 704 705 if name is None and value is None: 706 self._configure_moin(wikiconfig) 707 else: 708 wikiconfig.set(name, value, raw=raw) 709 710 finally: 711 wikiconfig.close() 712 713 def set_auth_method(self, method_name): 714 715 """ 716 Edit the installed Wiki configuration file, configuring the 717 authentication method having the given 'method_name'. 718 """ 719 720 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 721 722 status("Editing configuration from %s..." % wikiconfig_py) 723 724 wikiconfig = Configuration(wikiconfig_py) 725 726 try: 727 if method_name.lower() == "openid": 728 wikiconfig.insert_text("from MoinMoin.auth.openidrp import OpenIDAuth") 729 730 if wikiconfig.get("anonymous_session_lifetime"): 731 wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1) 732 else: 733 wikiconfig.set("anonymous_session_lifetime", "1000", raw=1) 734 735 auth = wikiconfig.get("auth") 736 if auth: 737 wikiconfig.replace("auth", "%s + [OpenIDAuth()]" % auth, raw=1) 738 else: 739 wikiconfig.set("auth", "[OpenIDAuth()]", raw=1) 740 741 finally: 742 wikiconfig.close() 743 744 def install_theme(self, theme_dir, theme_name=None): 745 746 """ 747 Install Wiki theme provided in the given 'theme_dir' having the given 748 optional 'theme_name' (if different from the 'theme_dir' name). 749 """ 750 751 theme_dir = normpath(theme_dir) 752 theme_name = theme_name or split(theme_dir)[-1] 753 theme_module = join(theme_dir, theme_name + extsep + "py") 754 755 plugin_theme_dir = self.get_plugin_directory("theme") 756 757 # Copy the theme module. 758 759 status("Copying theme module to %s..." % plugin_theme_dir) 760 761 shutil.copy(theme_module, plugin_theme_dir) 762 763 # Copy the resources. 764 765 resources_dir = join(self.htdocs_dir, theme_name) 766 767 if not exists(resources_dir): 768 os.mkdir(resources_dir) 769 770 status("Copying theme resources to %s..." % resources_dir) 771 772 for d in ("css", "img"): 773 target_dir = join(resources_dir, d) 774 if exists(target_dir): 775 status("Replacing %s..." % target_dir) 776 shutil.rmtree(target_dir) 777 shutil.copytree(join(theme_dir, d), target_dir) 778 779 # Copy additional resources from other themes. 780 781 resources_source_dir = join(self.htdocs_dir, self.theme_master) 782 target_dir = join(resources_dir, "css") 783 784 status("Copying resources from %s..." % resources_source_dir) 785 786 for css_file in self.extra_theme_css_files: 787 css_file_path = join(resources_source_dir, "css", css_file) 788 if exists(css_file_path): 789 shutil.copy(css_file_path, target_dir) 790 791 note("Don't forget to add theme resources for extensions for this theme.") 792 note("Don't forget to edit this theme's stylesheets for extensions.") 793 794 def install_extension_package(self, extension_dir): 795 796 "Install any libraries from 'extension_dir' using a setup script." 797 798 this_dir = os.getcwd() 799 os.chdir(extension_dir) 800 os.system("python setup.py install --prefix=%s" % self.prefix) 801 os.chdir(this_dir) 802 803 def install_plugins(self, plugins_dir, plugin_type): 804 805 """ 806 Install Wiki actions provided in the given 'plugins_dir' of the 807 specified 'plugin_type'. 808 """ 809 810 plugin_target_dir = self.get_plugin_directory(plugin_type) 811 812 # Copy the modules. 813 814 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 815 816 for module in glob(join(plugins_dir, "*%spy" % extsep)): 817 shutil.copy(module, plugin_target_dir) 818 819 def install_actions(self, actions_dir): 820 821 "Install Wiki actions provided in the given 'actions_dir'." 822 823 self.install_plugins(actions_dir, "action") 824 825 def install_macros(self, macros_dir): 826 827 "Install Wiki macros provided in the given 'macros_dir'." 828 829 self.install_plugins(macros_dir, "macro") 830 831 def install_theme_resources(self, theme_resources_dir, theme_name=None): 832 833 """ 834 Install theme resources provided in the given 'theme_resources_dir'. If 835 a specific 'theme_name' is given, only that theme will be given the 836 specified resources. 837 """ 838 839 for theme_name, theme_dir in self.get_theme_directories(theme_name): 840 841 # Copy the resources. 842 843 copied = 0 844 845 for d in ("css", "img"): 846 source_dir = join(theme_resources_dir, d) 847 target_dir = join(theme_dir, d) 848 849 if not exists(target_dir): 850 continue 851 852 for resource in glob(join(source_dir, "*%s*" % extsep)): 853 shutil.copy(resource, target_dir) 854 copied = 1 855 856 if copied: 857 status("Copied theme resources into %s..." % theme_dir) 858 859 note("Don't forget to edit theme stylesheets for any extensions.") 860 861 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 862 863 """ 864 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 865 the 'imported_stylesheet' according to the given 'action' (optional, 866 defaulting to "ensure"). If a specific 'theme_name' is given, only that 867 theme will be affected. 868 """ 869 870 if action == "ensure": 871 ensure = 1 872 elif action == "remove": 873 ensure = 0 874 else: 875 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 876 return 877 878 for theme_name, theme_dir in self.get_theme_directories(theme_name): 879 880 # Locate the resources. 881 882 css_dir = join(theme_dir, "css") 883 884 if not exists(css_dir): 885 continue 886 887 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 888 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 889 890 if not exists(theme_stylesheet_filename): 891 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 892 continue 893 894 if not exists(imported_stylesheet_filename): 895 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 896 continue 897 898 # Edit the resources. 899 900 s = readfile(theme_stylesheet_filename) 901 after_point = 0 902 903 for stylesheet_import in css_import_stylesheet.finditer(s): 904 before, filename, after = stylesheet_import.groups() 905 before_point, after_point = stylesheet_import.span() 906 907 # Test the import for a reference to the requested imported 908 # stylesheet. 909 910 if filename == imported_stylesheet: 911 if ensure: 912 break 913 else: 914 if s[after_point:after_point+1] == "\n": 915 after_point += 1 916 s = "%s%s" % (s[:before_point], s[after_point:]) 917 918 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 919 writefile(theme_stylesheet_filename, s) 920 break 921 922 # Where no import references the imported stylesheet, insert a 923 # reference into the theme stylesheet. 924 925 else: 926 if ensure: 927 928 # Assume that the stylesheet can follow other imports. 929 930 if s[after_point:after_point+1] == "\n": 931 after_point += 1 932 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 933 934 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 935 writefile(theme_stylesheet_filename, s) 936 937 def make_page_package(self, page_directory, package_filename): 938 939 """ 940 Make a package containing the pages in 'page_directory', using the 941 filenames as the page names, and writing the package to a file with the 942 given 'package_filename'. 943 """ 944 945 package = ZipFile(package_filename, "w") 946 947 try: 948 script = ["MoinMoinPackage|1"] 949 950 for filename in os.listdir(page_directory): 951 package.write(join(page_directory, filename), filename) 952 script.append("AddRevision|%s|%s" % (filename, filename)) 953 954 package.writestr("MOIN_PACKAGE", "\n".join(script)) 955 956 finally: 957 package.close() 958 959 def install_page_package(self, package_filename): 960 961 """ 962 Install a package from the file with the given 'package_filename'. 963 """ 964 965 path = os.environ.get("PYTHONPATH", "") 966 967 if path: 968 os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + self.conf_dir 969 else: 970 os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + self.conf_dir 971 972 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 973 os.system("python %s i %s" % (installer, package_filename)) 974 975 if path: 976 os.environ["PYTHONPATH"] = path 977 else: 978 del os.environ["PYTHONPATH"] 979 980 def show_methods(): 981 print "Methods:" 982 print 983 for method_name in Installation.method_names: 984 doc = getattr(Installation, method_name).__doc__.strip() 985 print "%-30s%-s" % (method_name, format(doc, 30)) 986 print 987 988 # Command line option syntax. 989 990 syntax_description = "[ -f <config-filename> ] ( <method> | --method=METHOD ) [ <method-argument> ... ]" 991 992 # Main program. 993 994 if __name__ == "__main__": 995 from ConfigParser import ConfigParser 996 import sys, cmdsyntax 997 998 # Check the command syntax. 999 1000 syntax = cmdsyntax.Syntax(syntax_description) 1001 try: 1002 matches = syntax.get_args(sys.argv[1:]) 1003 args = matches[0] 1004 except IndexError: 1005 print "Syntax:" 1006 print sys.argv[0], syntax_description 1007 print 1008 show_methods() 1009 sys.exit(1) 1010 1011 # Obtain configuration details. 1012 1013 try: 1014 config_filename = args.get("config-filename", "moinsetup.cfg") 1015 config = ConfigParser() 1016 config.read(config_filename) 1017 1018 # Obtain as many arguments as needed from the configuration. 1019 1020 config_arguments = dict(config.items("installation") + config.items("site")) 1021 method_arguments = args.get("method-argument", []) 1022 1023 # Attempt to initialise the configuration. 1024 1025 installation = Installation(**config_arguments) 1026 1027 except TypeError: 1028 print "Configuration settings:" 1029 print 1030 print Installation.__init__.__doc__ 1031 print 1032 sys.exit(1) 1033 1034 # Obtain the method. 1035 1036 try: 1037 method = getattr(installation, args["method"]) 1038 except AttributeError: 1039 show_methods() 1040 sys.exit(1) 1041 1042 try: 1043 method(*method_arguments) 1044 except TypeError: 1045 print "Method documentation:" 1046 print 1047 print method.__doc__ 1048 print 1049 raise 1050 1051 # vim: tabstop=4 expandtab shiftwidth=4