src/Pure/Tools/phabricator.scala
changeset 79512 bf91c1aec34b
parent 79496 4d82b743c5e1
child 79513 292605271dcf
equal deleted inserted replaced
79511:57ceacd4a17b 79512:bf91c1aec34b
    16 
    16 
    17 
    17 
    18 object Phabricator {
    18 object Phabricator {
    19   /** defaults **/
    19   /** defaults **/
    20 
    20 
    21   /* required packages */
    21   /* webservers */
       
    22 
       
    23   sealed abstract class Webserver {
       
    24     override def toString: String = title
       
    25     def title: String
       
    26     def short_name: String
       
    27     def system_name: String = short_name
       
    28 
       
    29     def packages(): List[String]
       
    30 
       
    31     def system_path: Path = Path.basic(system_name)
       
    32     def root_dir: Path = Path.explode("/etc") + system_path
       
    33     def sites_dir: Path = root_dir + Path.explode("sites-available")
       
    34 
       
    35     def restart(): Unit = Linux.service_restart(system_name)
       
    36 
       
    37     def systemctl(cmd: String): String = "systemctl " + cmd + " " + system_name
       
    38 
       
    39     def php_version(): String =
       
    40       Isabelle_System.bash("""php --run 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;'""")
       
    41         .check.out
       
    42 
       
    43     def php_config: String =
       
    44       "post_max_size = 32M\n" +
       
    45       "opcache.validate_timestamps = 0\n" +
       
    46       "memory_limit = 512M\n" +
       
    47       "max_execution_time = 120\n"
       
    48 
       
    49     def php_init(): Unit = ()
       
    50 
       
    51     def site_name(name: String): String = isabelle_phabricator_name(name = name)
       
    52 
       
    53     def site_conf(name: String): Path =
       
    54       sites_dir + Path.basic(isabelle_phabricator_name(name = name, ext = "conf"))
       
    55 
       
    56     def site_init(name: String, server_name: String, webroot: String): Unit
       
    57   }
       
    58 
       
    59   object Apache extends Webserver {
       
    60     override val title = "Apache"
       
    61     override val short_name = "apache"
       
    62     override def system_name = "apache2"
       
    63     override def packages(): List[String] = List("apache2", "libapache2-mod-php")
       
    64 
       
    65     override def php_init(): Unit = {
       
    66       val php_conf =
       
    67         Path.explode("/etc/php") + Path.basic(php_version()) +  // educated guess
       
    68         Path.basic(system_name) + Path.explode("conf.d") +
       
    69         Path.basic(isabelle_phabricator_name(ext = "ini"))
       
    70       File.write(php_conf, php_config)
       
    71     }
       
    72 
       
    73     override def site_init(name: String, server_name: String, webroot: String): Unit = {
       
    74       File.write(site_conf(name),
       
    75 """<VirtualHost *:80>
       
    76     ServerName """ + server_name + """
       
    77     ServerAdmin webmaster@localhost
       
    78     DocumentRoot """ + webroot + """
       
    79 
       
    80     ErrorLog ${APACHE_LOG_DIR}/error.log
       
    81     CustomLog ${APACHE_LOG_DIR}/access.log combined
       
    82 
       
    83     RewriteEngine on
       
    84     RewriteRule ^(.*)$  /index.php?__path__=$1  [B,L,QSA]
       
    85 </VirtualHost>
       
    86 
       
    87 # vim: syntax=apache ts=4 sw=4 sts=4 sr noet
       
    88 """)
       
    89       Isabelle_System.bash("""
       
    90         set -e
       
    91         a2enmod rewrite
       
    92         a2ensite """ + Bash.string(site_name(name))).check
       
    93     }
       
    94   }
       
    95 
       
    96   object Nginx extends Webserver {
       
    97     override val title = "Nginx"
       
    98     override val short_name = "nginx"
       
    99     override def packages(): List[String] = List("nginx", "php-fpm")
       
   100 
       
   101     override def site_init(name: String, server_name: String, webroot: String): Unit = {
       
   102       File.write(site_conf(name),
       
   103 """
       
   104 server {
       
   105   server_name """ + server_name + """;
       
   106   root """ + webroot + """;
       
   107 
       
   108   location / {
       
   109     index index.php;
       
   110     rewrite ^/(.*)$ /index.php?__path__=/$1 last;
       
   111   }
       
   112 
       
   113   location ~ \.php$ {
       
   114     include snippets/fastcgi-php.conf;
       
   115     fastcgi_pass unix:/var/run/php/php""" + php_version() + """-fpm.sock;
       
   116   }
       
   117 
       
   118   location /index.php {
       
   119     fastcgi_index  index.php;
       
   120 
       
   121     #required if PHP was built with --enable-force-cgi-redirect
       
   122     fastcgi_param  REDIRECT_STATUS    200;
       
   123 
       
   124     #variables to make the $_SERVER populate in PHP
       
   125     fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
       
   126     fastcgi_param  QUERY_STRING       $query_string;
       
   127     fastcgi_param  REQUEST_METHOD     $request_method;
       
   128     fastcgi_param  CONTENT_TYPE       $content_type;
       
   129     fastcgi_param  CONTENT_LENGTH     $content_length;
       
   130 
       
   131     fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
       
   132 
       
   133     fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
       
   134     fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
       
   135 
       
   136     fastcgi_param  REMOTE_ADDR        $remote_addr;
       
   137   }
       
   138 }
       
   139 """)
       
   140       Isabelle_System.bash(
       
   141         "ln -sf " + File.bash_path(site_conf(name)) + " /etc/nginx/sites-enabled/.").check
       
   142     }
       
   143   }
       
   144 
       
   145   val all_webservers: List[Webserver] = List(Apache, Nginx)
       
   146 
       
   147   def get_webserver(name: String): Webserver =
       
   148     all_webservers.find(w => w.short_name == name) getOrElse
       
   149       error("Bad webserver " + quote(name))
       
   150 
       
   151   val default_webserver: Webserver = Apache
       
   152 
       
   153 
       
   154 
       
   155   /* system packages */
    22 
   156 
    23   val packages_ubuntu_20_04: List[String] =
   157   val packages_ubuntu_20_04: List[String] =
    24     Docker_Build.packages :::
   158     Docker_Build.packages :::
    25     List(
   159     List(
    26       // https://secure.phabricator.com/source/phabricator/browse/master/scripts/install/install_ubuntu.sh 15e6e2adea61
   160       // https://secure.phabricator.com/source/phabricator/browse/master/scripts/install/install_ubuntu.sh 15e6e2adea61
    27       "git", "mysql-server", "apache2", "libapache2-mod-php", "php", "php-mysql",
   161       "git", "mysql-server", "php", "php-mysql", "php-gd", "php-curl", "php-apcu", "php-cli",
    28       "php-gd", "php-curl", "php-apcu", "php-cli", "php-json", "php-mbstring",
   162       "php-json", "php-mbstring",
    29       // more packages
   163       // more packages
    30       "php-xml", "php-zip", "python3-pygments", "ssh", "subversion", "python-pygments",
   164       "php-xml", "php-zip", "python3-pygments", "ssh", "subversion", "python-pygments",
    31       // mercurial build packages
   165       // mercurial build packages
    32       "make", "gcc", "python", "python2-dev", "python-docutils", "python-openssl")
   166       "make", "gcc", "python", "python2-dev", "python-docutils", "python-openssl")
    33 
   167 
    34   val packages_ubuntu_22_04: List[String] =
   168   val packages_ubuntu_22_04: List[String] =
    35     Docker_Build.packages :::
   169     Docker_Build.packages :::
    36     List(
   170     List(
    37       // https://secure.phabricator.com/source/phabricator/browse/master/scripts/install/install_ubuntu.sh 15e6e2adea61
   171       // https://secure.phabricator.com/source/phabricator/browse/master/scripts/install/install_ubuntu.sh 15e6e2adea61
    38       "git", "mysql-server", "apache2", "libapache2-mod-php", "php", "php-mysql",
   172       "git", "mysql-server", "php", "php-mysql", "php-gd", "php-curl", "php-apcu", "php-cli",
    39       "php-gd", "php-curl", "php-apcu", "php-cli", "php-json", "php-mbstring",
   173       "php-json", "php-mbstring",
    40       // more packages
   174       // more packages
    41       "php-xml", "php-zip", "python3-pygments", "ssh", "subversion")
   175       "php-xml", "php-zip", "python3-pygments", "ssh", "subversion")
    42 
   176 
    43   def packages: List[String] = {
   177   def packages(webserver: Webserver): List[String] = {
    44     val release = Linux.Release()
   178     val release = Linux.Release()
    45     if (release.is_ubuntu_20_04) packages_ubuntu_20_04
   179     val pkgs =
    46     else if (release.is_ubuntu_22_04) packages_ubuntu_22_04
   180       if (release.is_ubuntu_20_04) packages_ubuntu_20_04
    47     else error("Bad Linux version: expected Ubuntu 20.04 or 22.04 LTS")
   181       else if (release.is_ubuntu_22_04) packages_ubuntu_22_04
       
   182       else error("Bad Linux version: expected Ubuntu 20.04 or 22.04 LTS")
       
   183     pkgs ::: webserver.packages()
    48   }
   184   }
    49 
   185 
    50 
   186 
    51   /* global system resources */
   187   /* global system resources */
    52 
   188 
   108   sealed case class Config(name: String, root: Path) {
   244   sealed case class Config(name: String, root: Path) {
   109     def home: Path = root + Path.explode(phabricator_name())
   245     def home: Path = root + Path.explode(phabricator_name())
   110 
   246 
   111     def execute(command: String): Process_Result =
   247     def execute(command: String): Process_Result =
   112       Isabelle_System.bash("bin/" + command, cwd = home.file, redirect = true).check
   248       Isabelle_System.bash("bin/" + command, cwd = home.file, redirect = true).check
       
   249 
       
   250     def webroot: String = home.implode + "/webroot"
   113   }
   251   }
   114 
   252 
   115   def read_config(): List[Config] = {
   253   def read_config(): List[Config] = {
   116     if (global_config.is_file) {
   254     if (global_config.is_file) {
   117       for (entry <- Library.trim_split_lines(File.read(global_config)) if entry.nonEmpty)
   255       for (entry <- Library.trim_split_lines(File.read(global_config)) if entry.nonEmpty)
   224   def phabricator_setup(
   362   def phabricator_setup(
   225     options: Options,
   363     options: Options,
   226     name: String = default_name,
   364     name: String = default_name,
   227     root: String = "",
   365     root: String = "",
   228     repo: String = "",
   366     repo: String = "",
       
   367     webserver: Webserver = default_webserver,
   229     package_update: Boolean = false,
   368     package_update: Boolean = false,
   230     mercurial_source: String = "",
   369     mercurial_source: String = "",
   231     progress: Progress = new Progress
   370     progress: Progress = new Progress
   232   ): Unit = {
   371   ): Unit = {
   233     /* system environment */
   372     /* system environment */
   239     if (package_update) {
   378     if (package_update) {
   240       Linux.package_update(progress = progress)
   379       Linux.package_update(progress = progress)
   241       Linux.check_reboot_required()
   380       Linux.check_reboot_required()
   242     }
   381     }
   243 
   382 
   244     Linux.package_install(packages, progress = progress)
   383     Linux.package_install(packages(webserver), progress = progress)
   245     Linux.check_reboot_required()
   384     Linux.check_reboot_required()
   246 
   385 
   247 
   386 
   248     if (mercurial_source.nonEmpty) {
   387     if (mercurial_source.nonEmpty) {
   249       for { name <- List("mercurial", "mercurial-common") if Linux.package_installed(name) } {
   388       for { name <- List("mercurial", "mercurial-common") if Linux.package_installed(name) } {
   401   echo "Bad branch: \"$BRANCH\""
   540   echo "Bad branch: \"$BRANCH\""
   402   exit 1
   541   exit 1
   403 fi
   542 fi
   404 
   543 
   405 systemctl stop isabelle-phabricator-phd
   544 systemctl stop isabelle-phabricator-phd
   406 systemctl stop apache2
   545 """ + webserver.systemctl("stop"),
   407 """,
       
   408       body =
   546       body =
   409 """echo -e "\nUpgrading phabricator \"$NAME\" root \"$ROOT\" ..."
   547 """echo -e "\nUpgrading phabricator \"$NAME\" root \"$ROOT\" ..."
   410 for REPO in arcanist phabricator
   548 for REPO in arcanist phabricator
   411 do
   549 do
   412   cd "$ROOT/$REPO"
   550   cd "$ROOT/$REPO"
   416 done
   554 done
   417 echo -e "\nUpgrading storage ..."
   555 echo -e "\nUpgrading storage ..."
   418 "$ROOT/phabricator/bin/storage" upgrade --force
   556 "$ROOT/phabricator/bin/storage" upgrade --force
   419 """,
   557 """,
   420       exit =
   558       exit =
   421 """systemctl start apache2
   559         webserver.systemctl("start") + """
   422 systemctl start isabelle-phabricator-phd""")
   560 systemctl start isabelle-phabricator-phd""")
   423 
   561 
   424 
   562 
   425     /* PHP setup */
   563     /* webserver setup */
   426 
   564 
   427     val php_version =
   565     progress.echo(webserver.title + " setup ...")
   428       Isabelle_System.bash("""php --run 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;'""")
   566 
   429         .check.out
   567     val sites_dir = webserver.sites_dir
   430 
   568     if (!sites_dir.is_dir) error("Bad " + webserver + " sites directory " + sites_dir)
   431     val php_conf =
       
   432       Path.explode("/etc/php") + Path.basic(php_version) +  // educated guess
       
   433         Path.explode("apache2/conf.d") +
       
   434         Path.basic(isabelle_phabricator_name(ext = "ini"))
       
   435 
       
   436     File.write(php_conf,
       
   437       "post_max_size = 32M\n" +
       
   438       "opcache.validate_timestamps = 0\n" +
       
   439       "memory_limit = 512M\n" +
       
   440       "max_execution_time = 120\n")
       
   441 
       
   442 
       
   443     /* Apache setup */
       
   444 
       
   445     progress.echo("Apache setup ...")
       
   446 
       
   447     val apache_root = Path.explode("/etc/apache2")
       
   448     val apache_sites = apache_root + Path.explode("sites-available")
       
   449 
       
   450     if (!apache_sites.is_dir) error("Bad Apache sites directory " + apache_sites)
       
   451 
   569 
   452     val server_name = phabricator_name(name = name, ext = "localhost")  // alias for "localhost" for testing
   570     val server_name = phabricator_name(name = name, ext = "localhost")  // alias for "localhost" for testing
   453     val server_url = "http://" + server_name
   571     val server_url = "http://" + server_name
   454 
   572 
   455     File.write(apache_sites + Path.basic(isabelle_phabricator_name(name = name, ext = "conf")),
   573     webserver.php_init()
   456 """<VirtualHost *:80>
   574 
   457     ServerName """ + server_name + """
   575     webserver.site_init(name, server_name, config.webroot)
   458     ServerAdmin webmaster@localhost
       
   459     DocumentRoot """ + config.home.implode + """/webroot
       
   460 
       
   461     ErrorLog ${APACHE_LOG_DIR}/error.log
       
   462     CustomLog ${APACHE_LOG_DIR}/access.log combined
       
   463 
       
   464     RewriteEngine on
       
   465     RewriteRule ^(.*)$  /index.php?__path__=$1  [B,L,QSA]
       
   466 </VirtualHost>
       
   467 
       
   468 # vim: syntax=apache ts=4 sw=4 sts=4 sr noet
       
   469 """)
       
   470 
       
   471     Isabelle_System.bash( """
       
   472       set -e
       
   473       a2enmod rewrite
       
   474       a2ensite """ + Bash.string(isabelle_phabricator_name(name = name))).check
       
   475 
   576 
   476     config.execute("config set phabricator.base-uri " + Bash.string(server_url))
   577     config.execute("config set phabricator.base-uri " + Bash.string(server_url))
   477 
   578 
   478     Linux.service_restart("apache2")
   579     webserver.restart()
   479 
   580 
   480     progress.echo("\nFurther manual configuration via " + server_url)
   581     progress.echo("\nFurther manual configuration via " + server_url)
   481 
   582 
   482 
   583 
   483     /* PHP daemon */
   584     /* PHP daemon */
   498     val phd_command = command_setup(phd_name, body = """"$ROOT/phabricator/bin/phd" "$@" """)
   599     val phd_command = command_setup(phd_name, body = """"$ROOT/phabricator/bin/phd" "$@" """)
   499     try {
   600     try {
   500       Linux.service_install(phd_name,
   601       Linux.service_install(phd_name,
   501 """[Unit]
   602 """[Unit]
   502 Description=PHP daemon manager for Isabelle/Phabricator
   603 Description=PHP daemon manager for Isabelle/Phabricator
   503 After=syslog.target network.target apache2.service mysql.service
   604 After=syslog.target network.target """ + webserver.system_name + """.service mysql.service
   504 
   605 
   505 [Service]
   606 [Service]
   506 Type=oneshot
   607 Type=oneshot
   507 User=""" + daemon_user + """
   608 User=""" + daemon_user + """
   508 Group=""" + daemon_user + """
   609 Group=""" + daemon_user + """
   533         var repo = ""
   634         var repo = ""
   534         var package_update = false
   635         var package_update = false
   535         var name = default_name
   636         var name = default_name
   536         var options = Options.init()
   637         var options = Options.init()
   537         var root = ""
   638         var root = ""
       
   639         var webserver = default_webserver
   538 
   640 
   539         val getopts = Getopts("""
   641         val getopts = Getopts("""
   540 Usage: isabelle phabricator_setup [OPTIONS]
   642 Usage: isabelle phabricator_setup [OPTIONS]
   541 
   643 
   542   Options are:
   644   Options are:
   545     -R DIR       repository directory (default: """ + default_repo("NAME") + """)
   647     -R DIR       repository directory (default: """ + default_repo("NAME") + """)
   546     -U           full update of system packages before installation
   648     -U           full update of system packages before installation
   547     -n NAME      Phabricator installation name (default: """ + quote(default_name) + """)
   649     -n NAME      Phabricator installation name (default: """ + quote(default_name) + """)
   548     -o OPTION    override Isabelle system OPTION (via NAME=VAL or NAME)
   650     -o OPTION    override Isabelle system OPTION (via NAME=VAL or NAME)
   549     -r DIR       installation root directory (default: """ + default_root("NAME") + """)
   651     -r DIR       installation root directory (default: """ + default_root("NAME") + """)
   550 
   652     -w NAME      webserver name (""" +
   551   Install Phabricator as LAMP application (Linux, Apache, MySQL, PHP).
   653           all_webservers.map(w => quote(w.short_name)).mkString (" or ") +
       
   654           ", default: " + quote(default_webserver.short_name) + """)
       
   655 
       
   656   Install Phabricator as Linux service, based on webserver + PHP + MySQL.
   552 
   657 
   553   The installation name (default: """ + quote(default_name) + """) is mapped to a regular
   658   The installation name (default: """ + quote(default_name) + """) is mapped to a regular
   554   Unix user; this is relevant for public SSH access.
   659   Unix user; this is relevant for public SSH access.
   555 """,
   660 """,
   556           "M:" -> (arg => mercurial_source = (if (arg == ":") standard_mercurial_source else arg)),
   661           "M:" -> (arg => mercurial_source = (if (arg == ":") standard_mercurial_source else arg)),
   557           "R:" -> (arg => repo = arg),
   662           "R:" -> (arg => repo = arg),
   558           "U" -> (_ => package_update = true),
   663           "U" -> (_ => package_update = true),
   559           "n:" -> (arg => name = arg),
   664           "n:" -> (arg => name = arg),
   560           "o:" -> (arg => options = options + arg),
   665           "o:" -> (arg => options = options + arg),
   561           "r:" -> (arg => root = arg))
   666           "r:" -> (arg => root = arg),
       
   667           "w:" -> (arg => webserver = get_webserver(arg)))
   562 
   668 
   563         val more_args = getopts(args)
   669         val more_args = getopts(args)
   564         if (more_args.nonEmpty) getopts.usage()
   670         if (more_args.nonEmpty) getopts.usage()
   565 
   671 
   566         val progress = new Console_Progress
   672         val progress = new Console_Progress
   567 
   673 
   568         phabricator_setup(options, name = name, root = root, repo = repo,
   674         phabricator_setup(options, name = name, root = root, repo = repo, webserver = webserver,
   569           package_update = package_update, mercurial_source = mercurial_source, progress = progress)
   675           package_update = package_update, mercurial_source = mercurial_source, progress = progress)
   570       })
   676       })
   571 
   677 
   572 
   678 
   573 
   679