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