blob: 2bab58c0d80a89d8fcc5ad4823df74246c0aaaae [file] [log] [blame]
#!/usr/bin/env ruby
require 'tmpdir'
require 'optparse'
require 'etc'
$root = (ARGV.delete '--sudo') || (Process.uid == 0)
$apache_would_have_been_restarted = (ARGV.delete '--apache-restarted')
#########################################################################
#
# Sets up a macOS machine for whimsy development. Pass --help as an
# option to get a description of the arguments. See
# https://github.com/apache/whimsy/blob/master/SETUPMYMAC.md#readme
# for a more complete description.
#
#########################################################################
unless RUBY_PLATFORM.include? 'darwin'
STDERR.puts "This script is intended to be run on macOS"
exit 1
end
unless (RUBY_VERSION.split('.').map(&:to_i) <=> [2, 4, 1]) >= 0
STDERR.puts "Ruby 2.4.1 or later is required"
exit 1
end
WHIMSY = File.realpath File.expand_path('..', __dir__)
COMMAND = File.realpath($0)
ARGS = ARGV.dup
Dir.chdir WHIMSY
$dry_run = false
restart_apache = false
$brew_updated = false
force = {clean: false, prune: false, toucher: nil, ws: nil}
force[:svn] = true unless Dir.exists? '/srv/svn'
force[:git] = true unless Dir.exists? '/srv/git'
### Convenience methods
# output a header line, in color if available
def color(line)
if STDOUT.isatty and ENV['TERM'].to_s.include? 'color'
puts "\n\u001b[35;1m#{line}\u001b[0m"
else
puts "\n" + line
end
end
# echo a command and run it
def run *args
color "$ " + "#{'sudo ' if $root}" + Array(args).join(' ')
return if $dry_run
Kernel.system *args
end
# run brew, making sure that a brew update is done before the first command
def brew *args
if not $brew_updated
run 'brew', 'update'
$brew_updated = true
end
run 'brew', *args
end
# Switch to root
def sudo
if $root
yield
elsif $dry_run
ARGS.push '--apache-restarted' if $apache_would_have_been_restarted
system RbConfig.ruby, COMMAND, *ARGS, '--sudo'
$apache_would_have_been_restarted = true
else
system "sudo", RbConfig.ruby, COMMAND, *ARGS
exit $?.exitstatus unless $?.success?
end
end
### Parse options to determine how whimsy code is to be run
option = :www
OptionParser.new do |opts|
opts.banner = "Usage: #$0 [options]"
opts.on('-u', '--user', "Run whimsy under your user") do |opt|
option = :user
end
opts.on('-w', '--web', "Run whimsy under the Apache web user") do |opt|
option = :web
end
opts.on('-d', '--docker', "Run whimsy on docker") do |opt|
option = :docker
end
opts.on('--[no-]gem', '--gems', "Upgrade gem dependencies") do |opt|
force[:gems] = opt
end
opts.on('--[no-]bundle', '--bundler', "Upgrade bundler") do |opt|
force[:bundler] = opt
end
opts.on('--[no-]node', "Upgrade to the latest node.js") do |opt|
force[:node] = opt
end
opts.on('--[no-]passenger', "Upgrade to the latest Phusion Passenger") do |opt|
force[:passenger] = opt
end
opts.on('--[no-]ldap', "Reconfigure LDAP") do |opt|
force[:ldap] = opts
end
opts.on('--[no-]svn', "Checkout/update svn repositories") do |opt|
force[:svn] = opts
end
opts.on('--[no-]git', "Clone/pull git repositories") do |opt|
force[:git] = opts
end
opts.on('--[no-]source', "Pull the latest changes to the source code") do |opt|
force[:source] = opt
end
opts.on('--[no-]minutes', "Collate board minutes") do |opt|
force[:minutes] = opt
end
opts.on('--update-all', "Update everything") do |opt|
force.default = true
end
opts.on('--[no-]toucher', "Restart rack applications on source change") do |opt|
force[:toucher] = opt
end
opts.on('--[no-]ws', "Start board agenda websocket") do |opt|
force[:ws] = opt
end
opts.on('--all', "Update and launch everything") do |opt|
force[:ws] = true if force[:ws] == nil
force[:toucher] = true if force[:toucher] == nil
force.default = true
end
opts.on('--[no-]clean', "Clean up source directory") do |opt|
force[:clean] = opt
end
opts.on('--[no-]prune', "Prune docker containers and images") do |opt|
force[:prune] = opt
end
opts.on('--dry-run', "Only indicate what commands would be run") do |opt|
$dry_run = true
end
end.parse!
user = option == :www ? '_www' : (ENV['SUDO_USER'] || Etc.getlogin)
uid = Etc.getpwnam(user).uid
gid = Etc.getpwnam(user).gid
group = Etc.getgrgid(gid).name
sudo_user = ENV['SUDO_USER'] ?
Etc.getpwnam(ENV['SUDO_USER']) : Etc.getpwuid(Process.uid)
### Install Homebrew
if not $root and (option!=:docker or not Dir.exist? '/Applications/Docker.app')
if `which brew`.empty?
script = 'https://raw.githubusercontent.com/Homebrew/install/master/install'
color %($ ruby -e "$(curl -fsSL #{script})")
eval `curl -fsSL #{script}` unless $dry_run
end
end
## Install Node.js
if not $root and option != :docker
if `which n`.empty?
brew 'install', 'n'
elsif force[:node]
brew 'upgrade', 'n'
end
end
unless Dir.exist? '/usr/local/n'
sudo { run 'mkdir', '/usr/local/n' }
end
unless File.stat('/usr/local/n').uid == sudo_user.uid
sudo {run 'chown', '-R', "#{sudo_user.uid}:#{sudo_user.gid}", '/usr/local/n'}
end
if not $root and option != :docker
if `which node`.empty?
run 'n lts'
elsif force[:node] and "v#{`n ls-remote stable`}" != `node --version`
run 'n lts'
end
# Prompt for xcode installation
`git --version` unless $dry_run
# if not (svn installed and svnmucc is on path)
unless system 'svn --help > /dev/null 2>&1' and not `which svnmucc`.empty?
brew 'install', 'subversion'
end
end
## Install Passenger
if not $root and option != :docker
if `which passenger`.empty?
brew 'install', 'passenger'
elsif force[:passenger]
brew 'upgrade', 'passenger'
end
end
### Create /srv
mac_version = `sw_vers`[/ProductVersion:\s+(.*)/, 1]
unless Dir.exist? '/srv'
sudo do
if (mac_version.split('.').map(&:to_i) <=> [10, 15, 0]) >= 0
# Catalina or later
run 'mkdir', '/var/whimsy' unless Dir.exist? '/var/whimsy'
run 'chown', "#{sudo_user.uid}:#{sudo_user.gid}", '/var/whimsy'
run 'touch', '/etc/synthetic.conf'
SYNTHETIC = '/etc/synthetic.conf'
unless File.read(SYNTHETIC).include? "/var/whimsy"
color "$ sudo edit #{SYNTHETIC}"
unless $dry_run
File.write SYNTHETIC, File.read(SYNTHETIC) + "srv\t/var/whimsy\n"
end
STDERR.puts "#{SYNTHETIC} updated; reboot machine and rerun this script"
puts %(\nPress "y" to reboot now, anything else to exit)
run "shutdown -r now" if gets.strip.downcase == "y"
exit 1
end
else
# prior to Catalina
run 'mkdir', '/srv'
run 'chown', "#{sudo_user}:#{sudo_group}", '/srv'
end
end
end
# relocate whimsy clone
if not Dir.exist? '/srv/whimsy'
sudo do
run 'mv', WHIMSY, '/srv/whimsy'
run 'ln', '-s', '/srv/whimsy', WHIMSY
end
end
# clean source
if force[:clean] and not $root
Dir.chdir '/srv/whimsy' do
run 'git', 'reset', '--hard'
run 'git', 'clean', '-fxd'
end
end
# update source
if force[:source] and not $root
Dir.chdir '/srv/whimsy' do
run 'git', 'pull'
end
end
### Define directories
directories = [
'/srv/agenda',
'/srv/cache',
'/srv/mail',
'/srv/mail/secretary',
'/srv/secretary',
'/srv/secretary/tlpreq',
'/srv/whimsy/www/logs',
'/srv/whimsy/www/public',
]
files = [
'/srv/whimsy/www/status/status.json'
]
directories.each do |dir|
sudo {run 'mkdir', '-p', dir} unless Dir.exist? dir
unless File.stat(dir).uid == uid
sudo {run 'chown', '-R', "#{uid}:#{gid}", dir}
end
end
files.each do |file|
sudo {run 'touch', file} unless File.exist? file
unless File.stat(file).uid == uid
sudo {run 'chown', "#{uid}:#{gid}", file}
end
end
if not File.exist? '/srv/whimsy/www/members/log'
run 'ln -s /var/log/apache2 /srv/whimsy/www/members/log'
end
### Docker installation
if force[:prune]
Dir.chdir 'docker' do
`docker-compose ps -q`.lines.each do |line|
run 'docker', 'stop', line.chomp
end
end
run 'docker', 'container', 'prune', '--force'
run 'docker', 'imagea', 'prune', '--force'
exit
end
if option == :docker
if not Dir.exist? '/Applications/Docker.app'
brew 'cask', 'install', 'docker'
end
if `which docker-compose`.empty?
run 'open /Applications/Docker.app'
end
unless system 'docker info > /dev/null 2>&1'
run 'open /Applications/Docker.app'
end
if not $root
Dir.chdir '/srv/whimsy' do
run 'rake docker:update'
end
end
exit
end
### Configure passenger
passenger_conf = '/etc/apache2/other/passenger.conf'
if force[:passenger] or not File.exist? passenger_conf
if Process.uid == 0
instructions = `su $SUDO_USER -c "brew info passenger"`
else
instructions = `brew info passenger`
end
section = instructions[/To activate Phusion Passenger for Apache.*(\n\n|\z)/m]
snippet = section.scan(/^ .*/).join("\n") + "\n"
snippet[/Passenger\w*Ruby\s+(.*)/, 1] = RbConfig.ruby
if option != :user
snippet += "PassengerUser #{user}\nPassengerGroup #{group}\n"
end
if not File.exists?(passenger_conf) or File.read(passenger_conf) != snippet
sudo do
color "$ sudo edit #{passenger_conf}"
File.write passenger_conf, snippet unless $dry_run
restart_apache = true
end
end
end
### Install bundler
if `which bundle`.empty?
if File.writable? Gem.dir
run 'gem install bundler'
else
sudo {run 'gem install bundler'}
end
elsif force[:bundler]
if File.writable? Gem.dir
run 'gem update bundler'
else
sudo {run 'gem update bundler'}
end
ARGS.push '--no-bundle'
end
### Installl gems
if not $root
if force[:gems] or not File.exist?("#{WHIMSY}/Gemfile.lock")
Dir.chdir WHIMSY do
run "rake", "update"
end
end
end
### Checkout/clone repositories
if force[:svn] and not $root
run 'rake', 'svn:update'
end
if force[:git] and not $root
run 'rake', 'git:pull'
end
### Collate minutes
if not $root
if force[:minutes] or not Dir.exist? '/srv/whimsy/www/board/minutes'
run 'tools/collate_minutes.rb'
end
end
### Configure LDAP
if File.exist? "#{WHIMSY}/Gemfile.lock"
$LOAD_PATH.unshift '/srv/whimsy/lib'
require 'whimsy/asf'
if force[:ldap] or not ASF::LDAP.configured?
sudo do
color '$ ruby -I lib -r whimsy/asf -e "ASF::LDAP.configure"'
ASF::LDAP.configure unless $dry_run
end
ARGS.push '--no-ldap'
end
end
### Make whimsy.local an alias for your machine
hosts = File.read('/etc/hosts')
unless hosts.include? 'whimsy.local'
sudo do
color '$ sudo edit /etc/hosts'
hosts[/^[:\d].*\slocalhost\b.*()/, 1] = ' whimsy.local'
File.write '/etc/hosts', hosts unless $dry_run
end
end
### Configure httpd
HTTPD_CONF = '/etc/apache2/httpd.conf'
config = File.read(HTTPD_CONF)
# uncomment necessary modules
instructions = File.read(File.expand_path('../MACOSX.md', __dir__))
section = instructions[/^Configure whimsy.local vhost\n--+\n.*?\n--/m]
uncomment = section[/Uncomment.*?```(.*?)```/m, 1]
add = section[/Add.*?```(.*?)```/m, 1].strip
uncomment.scan(/^\S.*/).each do |line|
config.sub!(/^\s*#\s*#{line}\s*$/) { $&.sub('#', '') }
if config !~ /^\s*#{line}\s*$/
STDERR.puts "Not found: #{line}"
exit 1
end
end
config += "\n" unless config.end_with? "\n"
# add additional lines from the instructions
add.scan(/^\S.*/).each do |line|
if config !~ /^\s*#{line}\s*$/
config += "#{line}\n"
end
end
# run under the specified user
config[/^User\s+(.*)/, 1] = user
config[/^Group\s+(.*)/, 1] = group
# add index.cgi to DirectoryIndex
unless config =~ /^\s*DirectoryIndex\s+.*index\.cgi\b/i
config[/^\s*DirectoryIndex\s.*()/, 1] = ' index.cgi'
end
# replace configuration file if changed
if config != File.read(HTTPD_CONF)
sudo do
color "$ sudo edit #{HTTPD_CONF}"
return if $dry_run
File.rename HTTPD_CONF, HTTPD_CONF + ".original"
File.write(HTTPD_CONF, config)
end
restart_apache = true
end
wconf_source = "#{WHIMSY}/config/whimsy.conf"
wconf_content = File.read(wconf_source)
if wconf_content =~ /^\s*SetEnv PATH /i
wconf_content[/^\s*SetEnv PATH .*/] =
"SetEnv PATH #{File.dirname RbConfig.ruby}:" +
"#{File.dirname(`which svnmucc`)}:${PATH}"
end
wconf_target = '/private/etc/apache2/other/whimsy.conf'
if
not File.exist?(wconf_target) or
File.read(wconf_target) != wconf_content
then
sudo do
color "$ cp #{wconf_source} #{wconf_target}"
File.write wconf_target, wconf_content
end
restart_apache = true
end
confd_source = "#{WHIMSY}/config/25-authz_ldap_group_membership.conf"
confd_target = '/private/etc/apache2/other/25-authz_ldap_group_membership.conf'
if
not File.exist?(confd_target) or
File.read(confd_target) != File.read(confd_source)
then
sudo do
run 'cp', confd_source, confd_target
end
restart_apache = true
end
### Make applications restart on change
if not $root and force[:toucher] != nil
plist = "#{Dir.home}/Library/LaunchAgents/toucher.plist"
if force[:toucher]
contents = File.read("#{__dir__}/toucher.plist")
contents[/>(.*ruby.*)</, 1] = RbConfig.ruby
if not Dir.exist? File.dirname(plist)
run "mkdir -p #{File.dirname(plist)}"
end
if not File.exist?(plist) or File.read(plist) != contents
color "$ edit #{plist}"
File.write plist, contents unless $dry_run
if `launchctl list`.include? 'org.apache.whimsy/toucher'
run "launchctl unload #{plist}"
end
end
if not `launchctl list`.include? 'org.apache.whimsy/toucher'
run "launchctl load #{plist}"
end
else
if `launchctl list`.include? 'org.apache.whimsy/toucher'
run "launchctl unload #{plist}"
end
if File.exist?(plist)
run "rm #{plist}"
end
end
end
### Board Agenda websocket
if force[:ws] != nil
sudo do
plist = "/Library/LaunchDaemons/board-agenda-websocket.plist"
if force[:ws]
contents = File.read("#{__dir__}/board-agenda-websocket.plist")
contents[/>(.*ruby.*)</, 1] = RbConfig.ruby
contents[/<key>UserName<\/key>\s*<string>(.*?)<\/string>/, 1] = user
contents[/<key>GroupName<\/key>\s*<string>(.*?)<\/string>/, 1] = group
if not Dir.exist? File.dirname(plist)
run "mkdir -p #{File.dirname(plist)}"
end
if not File.exist?(plist) or File.read(plist) != contents
unless $dry_run
color "$ sudo edit #{plist}"
File.write plist, contents
end
if `launchctl list`.include? 'org.apache.whimsy/board/agenda'
run "launchctl unload #{plist}"
end
end
if not `launchctl list`.include? 'org.apache.whimsy/board/agenda'
run "launchctl load #{plist}"
end
else
if `launchctl list`.include? 'org.apache.whimsy/board/agenda'
run "launchctl unload #{plist}"
end
if File.exist?(plist)
run "rm #{plist}"
end
end
end
end
### Start Apache httpd
if $root and not $apache_would_have_been_restarted
if not `launchctl list`.include? 'org.apache.httpd'
run "launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist"
elsif restart_apache
run "apachectl restart"
sleep 0.5
end
elsif not $dry_run
system 'open http://whimsy.local/'
end