Created
March 13, 2016 02:21
-
-
Save smgoller/dd381cac1472a95f6af7 to your computer and use it in GitHub Desktop.
Demonstrate a wholly automated Xcode install.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -e | |
# Standalone script to install Xcode w/ CLI tools on a fresh OS X 10.8, via applescript. | |
# This might be more lovely (and maintainable) as ruby, if only we could have some handy | |
# gems that all need a compiler for native extensions. We don't, so do it old school. | |
# Some things: | |
# | |
# * This is handy: http://tldp.org/LDP/abs/html/ | |
# * So is: https://developer.apple.com/library/mac/documentation/<snip> | |
# <snip>AppleScript/Conceptual/AppleScriptLangGuide/ | |
# * We need sudo. | |
# * The creds should be passed as two lines on stdin; this is the most secure way I know. | |
# * Do not run this script as root. | |
# * Do not source this script into another (including your current shell.) | |
function show_help { | |
cat <<HELP | |
Usage: echo -e 'user\npass' | $0 please | |
Scripted installation of Xcode with CLI tools from the App Store. | |
Unless 'please' is passed, show this help and do nothing. | |
Otherwise this will look for your apple credentials on two lines in stdin (id on the | |
first line, then password), which will be verified with Apple. If nothing is supplied | |
on stdin, you will be prompted for credentials. | |
Using support for assistive devices and applescript, the App Store will be opened and | |
Xcode will be installed. Xcode will then be opened and license agreement will be | |
accepted for you: to review that agreement now, visit the url below. Finally, the | |
command-line utilities will be installed. | |
The Xcode license agreement: http://www.apple.com/legal/sla/docs/xcode.pdf | |
To enable support for assisitive devices, you may be prompted for your administrator | |
(sudo) password. | |
HELP | |
} | |
# Show the given message and exit with status 1. | |
function die { | |
echo -e "Error: $*" | |
exit 1 | |
} | |
# Show the given message, followed by help, and exit with status 1. | |
function die_help { | |
echo -e "Error: $*" | |
echo | |
show_help | |
exit 1 | |
} | |
[[ "$USER" == root ]] && die_help "Run this as a normal user, I'll sudo when I need to." | |
# A (very) poor man's headless browser. | |
# | |
# We follow redirects and deal with cookies. | |
# | |
# The first parameter should be a file for curl to use as a read-write cookie jar. | |
# Any remaining arguments (at least one more is required) are passed straight to curl. | |
# | |
# Since we use perl (HTML::Tree) elsewhere, we might be tempted to use LWP here and drop | |
# bash completely. However, getting the needed functionality simply (redirects, cookies) | |
# is apparently beyond the ken of several perlmonk threads. So curl it is. | |
function http { | |
[[ $# < 2 ]] && die "http helper doesn't understand '$@'" | |
local cookies="$1" | |
shift 1 | |
# --silent disables the progress bar | |
# --location follows redirects | |
# --cookie provides request cookies from the file | |
# --cookie-jar writes response cookies back afterwards | |
curl \ | |
--silent \ | |
--location \ | |
--cookie "$cookies" \ | |
--cookie-jar "$cookies" \ | |
"$@" | |
} | |
# Parse the given html content with a bit of perl. | |
# | |
# The html should be the first positional parameter. The perl should be passed on stdin | |
# (i.e. as an inline heredoc). It may assume an HTML::Tree named $doc which has parsed | |
# the content is in scope. | |
# | |
# Be sure to use single-quoted heredocs. | |
function html { | |
[[ $# == 1 ]] || die "html helper doesn't understand '$@'" | |
# If I try to not buffer stdin before calling perl, something craps the bed and stdin | |
# is lost. So grab it up front and use -e instead. Pray you don't need newlines. | |
local script=$(cat) | |
echo "$1" | perl -Mv5.12 -MHTML::Tree \ | |
-e 'my $doc = HTML::Tree->new();' \ | |
-e '$doc->parse_file(\*STDIN);' \ | |
-e "$script" | |
} | |
# Verify the credentials with Apple. | |
# | |
# Do this by simulating a login session to https://appleid.apple.com/. | |
# | |
# To acomplish this we use one of the few html parsing APIs available to a fresh 10.8 | |
# install: the HTML::Tree module in perl. | |
function verify_credentials { | |
[[ $# == 2 ]] || die "verify_credentials doesn't understand '$@'" | |
# Parse arguments | |
local apple_id="$1" | |
local apple_password="$2" | |
# Create the cookie file. | |
local cookie_jar=$(mktemp /tmp/install-xcode.XXXXXX) | |
# Try to clean it up on exit. Note we only get one exit handler per process. | |
trap "rm '$cookie_jar'" exit | |
# Go to the apple id management app front page. | |
local response=$(http "$cookie_jar" 'https://appleid.apple.com/') | |
# Find the "Manage your Apple ID" link. | |
local url=$(html "$response" <<'PERL' | |
# Consider links.. | |
for ( $doc->look_down('_tag' => 'a') ) { | |
# ..whose anchor text matches "Manage your Apple ID" | |
say $_->attr('href') if $_->as_text() =~ m/Manage your Apple ID/; | |
} | |
PERL | |
) | |
# Click it | |
response=$(http "$cookie_jar" "$url") | |
# Find the signIn field. Grab its action url as well as all of its fields | |
local url_and_query=$(html "$response" <<'PERL' | |
use URI::Escape; | |
for my $form ( $doc->look_down('_tag' => 'form') ) { | |
# Skip anyone with the wrong id | |
next unless $form->attr('id') =~ m/signIn/; | |
# Grab the url | |
say $form->attr('action'); | |
my @parameters = (); | |
# Examine the form's fields to build up a query string to POST | |
for my $input ( $form->look_down('_tag' => 'input') ) { | |
my $name = uri_escape($input->attr('name')); | |
# Skip these two, since we'll do them explictly after | |
next if $name =~ m/theAccountName/ || $name =~ m/theAccountPW/; | |
if ( defined($input->attr('value')) ) { | |
my $value = uri_escape($input->attr('value')); | |
push(@parameters, $name . "=" . $value); | |
} else { | |
push(@parameters, $name); | |
} | |
} | |
# Print the parameters together as one query string | |
say join("&", @parameters); | |
# Ok, we're done. Break the loop. | |
last; | |
} | |
PERL | |
) | |
# Parse the url out of the two-line result, (clumsily) resolve relative urls | |
url=$(echo "$url_and_query" | head -n 1 | sed 's|^/|https://appleid.apple.com/|') | |
# Parse query string out of the two-line result | |
local query="$(echo "$url_and_query" | tail -n 1)$creds" | |
# "Submit" the form | |
response=$(http "$cookie_jar" \ | |
-d "$query" \ | |
-d "theAccountName=$apple_id" \ | |
-d "theAccountPW=$apple_password" \ | |
"$url" | |
) | |
html "$response" <<'PERL' | |
for ( $doc->look_down('_tag' => 'a') ) { | |
# We failed if we see a forgot password link. | |
exit 1 if $_->as_text() =~ m/Forgot your password/; | |
} | |
PERL | |
} | |
# Toggle support for assistive devices, a prerequisite for using applescript. | |
# | |
# This requires sudo. | |
# | |
# This is the same as the checkbox in the bottom left corner of the Universal Access | |
# system preferences panel. | |
function toggle_assistive_devices { | |
[[ $# == 1 ]] || die "toggle_assistive_devices doesn't understand '$@'" | |
local magic_file="/private/var/db/.AccessibilityAPIEnabled" | |
if [[ "$1" == "on" ]] | |
then | |
echo -n a | sudo tee "$magic_file" > /dev/null 2>&1 | |
sudo chmod 444 "$magic_file" | |
elif [[ "$1" == "off" ]] | |
then | |
sudo rm -f "$magic_file" | |
else | |
die "toggle_assistive_devices doesn't understand '$@'" | |
fi | |
} | |
# Toggle requiring an administrator password for installing Apple software. | |
# | |
# This is the password prompt that Xcode presents when trying to install the command line | |
# tools. | |
# | |
# Requires sudo. | |
# | |
# Don't toggle this on without first toggling it off first. | |
function toggle_install_apple_software_check { | |
[[ $# == 1 ]] || die "toggle_install_apple_software_check doesn't understand '$@'" | |
local reverse="" | |
if [[ "$1" == "on" ]] | |
then | |
reverse="-R" | |
elif [[ "$1" == "off" ]] | |
then | |
: # no-op | |
else | |
die "toggle_install_apple_software_check doesn't understand '$@'" | |
fi | |
sudo patch $reverse -l -d / -p1 <<'PATCH' 2>&1 >/dev/null | |
--- a/etc/authorization 2013-09-11 23:04:16.000000000 -0700 | |
+++ b/etc/authorization 2013-09-11 23:38:59.000000000 -0700 | |
@@ -5190,7 +5190,7 @@ | |
<key>system.install.apple-software</key> | |
<dict> | |
<key>class</key> | |
- <string>rule</string> | |
+ <string>allow</string> | |
<key>comment</key> | |
<string>Checked when user is installing Apple-provided software.</string> | |
<key>default-button</key> | |
PATCH | |
} | |
# Open the App Store and download Xcode. | |
function download_xcode { | |
[[ $# == 2 ]] || die "download_xcode doesn't understand '$@'" | |
# Parse arguments | |
local apple_id="$1" | |
local apple_password="$2" | |
# Do we already have Xcode? We're done! | |
[[ -d /Applications/Xcode.app ]] && return 0 | |
# Open the Xcode page within the App Store | |
open 'macappstore://itunes.apple.com/us/app/xcode/id497799835' | |
# Give it a moment | |
sleep 2 | |
echo -e "$apple_id\n$apple_password" | osascript 3<&0 <<'APPLESCRIPT' | |
on run argv | |
# Parse arguments | |
set stdin to do shell script "cat 0<&3" | |
set appleId to paragraph 1 of stdin | |
set applePassword to paragraph 2 of stdin | |
tell application "System Events" | |
tell window "App Store" of process "App Store" | |
set loaded to false | |
repeat until loaded = true | |
try | |
# There's really no less brittle way I can find to navigate the UI. | |
# Along with the Accessibility Inspector, "UI elements" is your friend: | |
# i.e. "tell scroll area 1 to UI elements" | |
# http://n8henrie.com/2013/03/a-strategy-for-ui-scripting-in-applescript/ | |
set installButtonContainer to group 1 of group 1 of UI element 1 of scroll area 1 | |
set installButton to button 1 of installButtonContainer | |
set loaded to true | |
on error | |
delay 1 | |
end try | |
end repeat | |
if description of installButton = "Installed, Xcode" then | |
tell application "App Store" to quit | |
return # It claims to be installed. | |
end if | |
if not description of installButton = "Install, Xcode, Free" then | |
# What page are we looking at? Is Xcode no longer free? Bail. | |
error "Can't find install button." | |
end if | |
# Petrov: Sir! The reason for having two keys is so that no one man may... | |
click installButton | |
# Give it a moment. | |
delay 2 | |
# Regrab the reference, since they may have replaced it | |
set installButton to button 1 of installButtonContainer | |
# Do we need to confirm? | |
if description of installButton = "Confirm, Install, Xcode, Free" then | |
# Ramius: May what, Doctor? | |
click installButton | |
# Give it a moment. | |
delay 2 | |
# Regrab the reference, since they may have replaced it | |
set installButton to button 1 of installButtonContainer | |
end if | |
# Do we need to authenticate? | |
# If so, we will be looking at a modal pop-down dialog for credentials. | |
set needToAuthenticate to false | |
try | |
# We should now be looking at a modal pop-down dialog for credentials. | |
set appleIdBox to text field 2 of sheet 1 | |
set applePasswordBox to text field 1 of sheet 1 | |
set signInButton to button 1 of sheet 1 | |
set needToAuthenticate to true | |
on error | |
# We may not be prompted for creds at all | |
end try | |
if needToAuthenticate = true then | |
# Petrov: Arm the missiles, Captain. | |
set value of appleIdBox to appleId | |
set value of applePasswordBox to applePassword | |
# Give it a moment. | |
delay 1 | |
# Ramius: Mmm, thank you for your concern Doctor. | |
click signInButton | |
# Give it a moment. | |
delay 10 | |
# Regrab the reference | |
set installButton to button 1 of installButtonContainer | |
end if | |
# At this point it should be downloading | |
if not description of installButton = "Installing, Xcode" then | |
error "Could not start install." | |
end if | |
# Busy wait.. | |
repeat while description of installButton = "Installing, Xcode" | |
delay 5 | |
set installButton to button 1 of installButtonContainer | |
end repeat | |
if description of installButton = "Install, Xcode, Free" then | |
error "Install paused or cancelled" | |
else if not description of installButton = "Installed, Xcode" then | |
error "Unknown error during installation" | |
end if | |
end tell | |
end tell | |
tell application "App Store" to quit | |
end run | |
APPLESCRIPT | |
} | |
# Open Xcode, accept the license if needed and install the command line tools | |
function install_command_line_tools { | |
[[ $# == 0 ]] || die "install_command_line_tools doesn't understand '$@'" | |
# Do we already have (say) make and gcc? We're done! | |
[ -r /usr/bin/make ] && [ -r /usr/bin/gcc ] && return 0 | |
# Open Xcode | |
open /Applications/Xcode.app | |
# Give it a moment | |
sleep 10 | |
osascript <<'APPLESCRIPT' | |
on run argv | |
tell application "System Events" | |
# Open Preferences | |
keystroke "," using command down | |
# Give it a moment | |
delay 1 | |
# The window name changes based on selected tab, use the number. | |
tell window 1 of process "Xcode" | |
click button "Downloads" of tool bar 1 | |
end tell | |
# Note the window reference has changed | |
tell window "Downloads" of process "Xcode" | |
set cliToolsRow to row 1 of table 1 of scroll area 1 of splitter group 1 | |
# There may be no button if installion is complete or in progress | |
try | |
click button "Install" of cliToolsRow | |
on error | |
# Nothing to do? Try falling through | |
end try | |
end tell | |
# # We may have to authenticate | |
# set needToAuthenticate to false | |
# try | |
# set authenticationPopup to window 1 of process "SecurityAgent" | |
# set needToAuthenticate to true | |
# on error | |
# # Then again maybe we don't | |
# end try | |
# | |
# if needToAuthenticate = true then | |
# tell authenticationPopup | |
# set adminPasswordBox to text field 2 of scroll area 1 of group 1 | |
# set installButton to button "Install Software" of group 2 | |
# | |
# set value of adminPasswordBox to adminPassword | |
# click installButton | |
# end tell | |
# end if | |
# Busy wait.. | |
tell window "Downloads" of process "Xcode" | |
set installed to false | |
repeat until installed = true | |
delay 1 | |
try | |
set cliToolsRow to row 1 of table 1 of scroll area 1 of splitter group 1 | |
set progressIndicator to static text 2 of cliToolsRow | |
if value of progressIndicator = "Installed" then | |
set installed to true | |
else | |
error "Unknown installation error" | |
end if | |
on error | |
# No text field has appeared yet, it's probably the progress bar. | |
end try | |
end repeat | |
end tell | |
end tell | |
tell application "Xcode" to quit | |
end run | |
APPLESCRIPT | |
} | |
function main { | |
# Assert the only argument is 'please' or show the help and bomb out. | |
[[ $# != 1 || "$1" != 'please' ]] && show_help && exit 0 | |
local apple_id # The user's apple id from stdin | |
local apple_password # The user's apple password, from stdin | |
# Spawn a sudo refresh loop | |
#while true | |
#do | |
# sudo -v | |
# sleep 30 | |
#done & | |
# Detect interactive shells and either read in the credentials or prompt | |
if [ -t 0 ] | |
then | |
# Interactive | |
read -p 'Apple ID: ' apple_id | |
read -s -p 'Apple Password: ' apple_password | |
echo | |
else | |
# Non-interactive | |
read apple_id && read apple_password || | |
die_help 'Please pass your apple credentials on standard in.' | |
fi | |
# Verify the credentials or die | |
verify_credentials "$apple_id" "$apple_password" || | |
die 'Could not verify your credentials with Apple. Sorry!' | |
# Turn on support for assistive devices. | |
toggle_assistive_devices on | |
# Disable administrator auth check for installing command line tools | |
toggle_install_apple_software_check off | |
# Ensure Xcode is downloaded | |
download_xcode "$apple_id" "$apple_password" | |
# TODO: need to script accepting license. | |
# Accept the license if needed and install command line tools | |
install_command_line_tools | |
# Put the administrator auth check back | |
toggle_install_apple_software_check on | |
# Turn assistive devices back off | |
toggle_assistive_devices off | |
# Kill the sudo refresh loop | |
#kill %1 | |
#wait | |
} | |
main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment