| #!/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 |
| `svn --version` unless $dry_run |
| 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_useruid}:#{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/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_target = '/private/etc/apache2/other/whimsy.conf' |
| if |
| not File.exist?(wconf_target) or |
| File.read(wconf_target) != File.read(wconf_source) |
| then |
| sudo do |
| run 'cp', wconf_source, wconf_target |
| 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 |