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 660 writefile(site_def, s) 661 662 def make_post_install_script(self): 663 664 "Write a post-install script with additional actions." 665 666 this_user = os.environ["USER"] 667 postinst_script = "moinsetup-post.sh" 668 669 s = "#!/bin/sh\n" 670 671 for d in ("data", "underlay"): 672 s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, join(self.conf_dir, d)) 673 s += "chmod -R g+w '%s'\n" % join(self.conf_dir, d) 674 675 if not self.moin_version.startswith("1.9"): 676 s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, self.htdocs_dir) 677 678 writefile(postinst_script, s) 679 os.chmod(postinst_script, 0755) 680 note("Run %s as root to set file ownership and permissions." % postinst_script) 681 682 # Accessory methods. 683 684 def reconfigure_moin(self, name=None, value=None, raw=0): 685 686 """ 687 Edit the installed Wiki configuration file, setting a parameter with any 688 given 'name' to the given 'value', treating the value as a raw 689 expression (not a string) if 'raw' is set to a true value. 690 691 If 'name' and the remaining parameters are omitted, the default 692 configuration activity is performed. 693 """ 694 695 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 696 697 status("Editing configuration from %s..." % wikiconfig_py) 698 699 wikiconfig = Configuration(wikiconfig_py) 700 701 try: 702 # Perform default configuration. 703 704 if name is None and value is None: 705 self._configure_moin(wikiconfig) 706 else: 707 wikiconfig.set(name, value, raw=raw) 708 709 finally: 710 wikiconfig.close() 711 712 def set_auth_method(self, method_name): 713 714 """ 715 Edit the installed Wiki configuration file, configuring the 716 authentication method having the given 'method_name'. 717 """ 718 719 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 720 721 status("Editing configuration from %s..." % wikiconfig_py) 722 723 wikiconfig = Configuration(wikiconfig_py) 724 725 try: 726 if method_name.lower() == "openid": 727 wikiconfig.insert_text("from MoinMoin.auth.openidrp import OpenIDAuth") 728 729 if wikiconfig.get("anonymous_session_lifetime"): 730 wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1) 731 else: 732 wikiconfig.set("anonymous_session_lifetime", "1000", raw=1) 733 734 auth = wikiconfig.get("auth") 735 if auth: 736 wikiconfig.replace("auth", "%s + [OpenIDAuth()]" % auth, raw=1) 737 else: 738 wikiconfig.set("auth", "[OpenIDAuth()]", raw=1) 739 740 finally: 741 wikiconfig.close() 742 743 def install_theme(self, theme_dir, theme_name=None): 744 745 """ 746 Install Wiki theme provided in the given 'theme_dir' having the given 747 optional 'theme_name' (if different from the 'theme_dir' name). 748 """ 749 750 theme_dir = normpath(theme_dir) 751 theme_name = theme_name or split(theme_dir)[-1] 752 theme_module = join(theme_dir, theme_name + extsep + "py") 753 754 plugin_theme_dir = self.get_plugin_directory("theme") 755 756 # Copy the theme module. 757 758 status("Copying theme module to %s..." % plugin_theme_dir) 759 760 shutil.copy(theme_module, plugin_theme_dir) 761 762 # Copy the resources. 763 764 resources_dir = join(self.htdocs_dir, theme_name) 765 766 if not exists(resources_dir): 767 os.mkdir(resources_dir) 768 769 status("Copying theme resources to %s..." % resources_dir) 770 771 for d in ("css", "img"): 772 target_dir = join(resources_dir, d) 773 if exists(target_dir): 774 status("Replacing %s..." % target_dir) 775 shutil.rmtree(target_dir) 776 shutil.copytree(join(theme_dir, d), target_dir) 777 778 # Copy additional resources from other themes. 779 780 resources_source_dir = join(self.htdocs_dir, self.theme_master) 781 target_dir = join(resources_dir, "css") 782 783 status("Copying resources from %s..." % resources_source_dir) 784 785 for css_file in self.extra_theme_css_files: 786 css_file_path = join(resources_source_dir, "css", css_file) 787 if exists(css_file_path): 788 shutil.copy(css_file_path, target_dir) 789 790 note("Don't forget to add theme resources for extensions for this theme.") 791 note("Don't forget to edit theme stylesheets for any extensions.") 792 793 def install_extension_package(self, extension_dir): 794 795 "Install any libraries from 'extension_dir' using a setup script." 796 797 this_dir = os.getcwd() 798 os.chdir(extension_dir) 799 os.system("python setup.py install --prefix=%s" % self.prefix) 800 os.chdir(this_dir) 801 802 def install_plugins(self, plugins_dir, plugin_type): 803 804 """ 805 Install Wiki actions provided in the given 'plugins_dir' of the 806 specified 'plugin_type'. 807 """ 808 809 plugin_target_dir = self.get_plugin_directory(plugin_type) 810 811 # Copy the modules. 812 813 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 814 815 for module in glob(join(plugins_dir, "*%spy" % extsep)): 816 shutil.copy(module, plugin_target_dir) 817 818 def install_actions(self, actions_dir): 819 820 "Install Wiki actions provided in the given 'actions_dir'." 821 822 self.install_plugins(actions_dir, "action") 823 824 def install_macros(self, macros_dir): 825 826 "Install Wiki macros provided in the given 'macros_dir'." 827 828 self.install_plugins(macros_dir, "macro") 829 830 def install_theme_resources(self, theme_resources_dir, theme_name=None): 831 832 """ 833 Install theme resources provided in the given 'theme_resources_dir'. If 834 a specific 'theme_name' is given, only that theme will be given the 835 specified resources. 836 """ 837 838 for theme_name, theme_dir in self.get_theme_directories(theme_name): 839 840 # Copy the resources. 841 842 copied = 0 843 844 for d in ("css", "img"): 845 source_dir = join(theme_resources_dir, d) 846 target_dir = join(theme_dir, d) 847 848 if not exists(target_dir): 849 continue 850 851 for resource in glob(join(source_dir, "*%s*" % extsep)): 852 shutil.copy(resource, target_dir) 853 copied = 1 854 855 if copied: 856 status("Copied theme resources into %s..." % theme_dir) 857 858 note("Don't forget to edit theme stylesheets for any extensions.") 859 860 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 861 862 """ 863 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 864 the 'imported_stylesheet' according to the given 'action' (optional, 865 defaulting to "ensure"). If a specific 'theme_name' is given, only that 866 theme will be affected. 867 """ 868 869 if action == "ensure": 870 ensure = 1 871 elif action == "remove": 872 ensure = 0 873 else: 874 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 875 return 876 877 for theme_name, theme_dir in self.get_theme_directories(theme_name): 878 879 # Locate the resources. 880 881 css_dir = join(theme_dir, "css") 882 883 if not exists(css_dir): 884 continue 885 886 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 887 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 888 889 if not exists(theme_stylesheet_filename): 890 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 891 continue 892 893 if not exists(imported_stylesheet_filename): 894 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 895 continue 896 897 # Edit the resources. 898 899 s = readfile(theme_stylesheet_filename) 900 after_point = 0 901 902 for stylesheet_import in css_import_stylesheet.finditer(s): 903 before, filename, after = stylesheet_import.groups() 904 before_point, after_point = stylesheet_import.span() 905 906 # Test the import for a reference to the requested imported 907 # stylesheet. 908 909 if filename == imported_stylesheet: 910 if ensure: 911 break 912 else: 913 if s[after_point:after_point+1] == "\n": 914 after_point += 1 915 s = "%s%s" % (s[:before_point], s[after_point:]) 916 917 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 918 writefile(theme_stylesheet_filename, s) 919 break 920 921 # Where no import references the imported stylesheet, insert a 922 # reference into the theme stylesheet. 923 924 else: 925 if ensure: 926 927 # Assume that the stylesheet can follow other imports. 928 929 if s[after_point:after_point+1] == "\n": 930 after_point += 1 931 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 932 933 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 934 writefile(theme_stylesheet_filename, s) 935 936 def make_page_package(self, page_directory, package_filename): 937 938 """ 939 Make a package containing the pages in 'page_directory', using the 940 filenames as the page names, and writing the package to a file with the 941 given 'package_filename'. 942 """ 943 944 package = ZipFile(package_filename, "w") 945 946 try: 947 script = ["MoinMoinPackage|1"] 948 949 for filename in os.listdir(page_directory): 950 package.write(join(page_directory, filename), filename) 951 script.append("AddRevision|%s|%s" % (filename, filename)) 952 953 package.writestr("MOIN_PACKAGE", "\n".join(script)) 954 955 finally: 956 package.close() 957 958 def install_page_package(self, package_filename): 959 960 """ 961 Install a package from the file with the given 'package_filename'. 962 """ 963 964 path = os.environ.get("PYTHONPATH", "") 965 966 if path: 967 os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + self.conf_dir 968 else: 969 os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + self.conf_dir 970 971 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 972 os.system("python %s i %s" % (installer, package_filename)) 973 974 if path: 975 os.environ["PYTHONPATH"] = path 976 else: 977 del os.environ["PYTHONPATH"] 978 979 def show_methods(): 980 print "Methods:" 981 print 982 for method_name in Installation.method_names: 983 doc = getattr(Installation, method_name).__doc__.strip() 984 print "%-30s%-s" % (method_name, format(doc, 30)) 985 print 986 987 # Command line option syntax. 988 989 syntax_description = "[ -f <config-filename> ] ( <method> | --method=METHOD ) [ <method-argument> ... ]" 990 991 # Main program. 992 993 if __name__ == "__main__": 994 from ConfigParser import ConfigParser 995 import sys, cmdsyntax 996 997 # Check the command syntax. 998 999 syntax = cmdsyntax.Syntax(syntax_description) 1000 try: 1001 matches = syntax.get_args(sys.argv[1:]) 1002 args = matches[0] 1003 except IndexError: 1004 print "Syntax:" 1005 print sys.argv[0], syntax_description 1006 print 1007 show_methods() 1008 sys.exit(1) 1009 1010 # Obtain configuration details. 1011 1012 try: 1013 config_filename = args.get("config-filename", "moinsetup.cfg") 1014 config = ConfigParser() 1015 config.read(config_filename) 1016 1017 # Obtain as many arguments as needed from the configuration. 1018 1019 config_arguments = dict(config.items("installation") + config.items("site")) 1020 method_arguments = args.get("method-argument", []) 1021 1022 # Attempt to initialise the configuration. 1023 1024 installation = Installation(**config_arguments) 1025 1026 except TypeError: 1027 print "Configuration settings:" 1028 print 1029 print Installation.__init__.__doc__ 1030 print 1031 sys.exit(1) 1032 1033 # Obtain the method. 1034 1035 try: 1036 method = getattr(installation, args["method"]) 1037 except AttributeError: 1038 show_methods() 1039 sys.exit(1) 1040 1041 try: 1042 method(*method_arguments) 1043 except TypeError: 1044 print "Method documentation:" 1045 print 1046 print method.__doc__ 1047 print 1048 raise 1049 1050 # vim: tabstop=4 expandtab shiftwidth=4