diff --git a/.gitignore b/.gitignore
index 1fd9f3e..b59f103 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
# ANSIBLE_HOME, if it's set here.
.ansible/
+.virtualenv/
diff --git a/.idea/mac-setup.iml b/.idea/mac-setup.iml
index 0e0325b..9a687e3 100644
--- a/.idea/mac-setup.iml
+++ b/.idea/mac-setup.iml
@@ -4,6 +4,7 @@
+
diff --git a/README.md b/README.md
index bf2dc22..47057ac 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,8 @@ Tested on Sequoia. Requires python3.
1. Run `init.sh`, which does the following:
1. Accepts the Xcode license (may prompt for admin password)
- 1. Installs ansible (`pip3 install --user ansible`)
- 1. Installs task dependencies in `requirements.txt` (again, with `pip3 install --user`).
+ 1. Installs ansible in a virtual environment (in the `.virtualenv` subdirectory)
+ 1. Installs task dependencies in `requirements.txt` (in the virtual environment)
1. Installs dependencies in `requirements.yml` using `ansible-galaxy`.
1. Optionally grant users the ability to use sudo with `sudoers.sh -K`. See `sudoers-playbook.yml` for
options.
@@ -79,6 +79,20 @@ Tested on Sequoia. Requires python3.
1. [JetBrains Mono](https://www.jetbrains.com/lp/mono/)
1. [DejaVu](https://dejavu-fonts.github.io/)
+## Virtual environment
+
+Our setup uses a python [virtual environment](https://docs.python.org/3/library/venv.html), created when you run
+`init.sh`. The python executable, libraries, etc., are _copied_ into this environment, protecting us against changes to
+the original files (e.g., if we upgrade the macports python version, we risk breaking symlinks in a linked virtual
+environment, whereas copies will continue to function).
+
+Each run of `init.sh` will try to create this virtual environment if it does not already exist. This is determined by
+checking for an executable `pip3` in the virtual environment's `bin` directory.
+
+You can pass the `-u|--upgrade-venv` option to this script. If the version of python in use is newer than that used to
+create the virtual environment, the virtual environment will be upgraded to use the newer python version. Note that
+this fails with a (harmless) error if the virtual environment's python executable is the same as the python executable
+on the path.
## Python versions
@@ -96,9 +110,10 @@ steps to get properly set up is
1. Run `init.sh` to install ansible for the default (`3.9`) python.
1. Run `bootstrap.sh` to bootstrap using this installation. This installs a newer python version.
-1. Run `init.sh` again to install ansible for the new python.
+1. (Optional) Remove (or rename for backup purposes) the `3.9` virtual environment in `.virtualenv`.
+1. Run `init.sh -u` to recreate the virtual environment with the newer python version.
1. (Optional) install the development dependencies by running `dev-init.sh`.
-1. Run `setup.sh` as needed. This will run the ansible installed for the newer python version.
+1. Run `setup.sh` as needed. This will now use the newer python virtual environment.
### Upgrading python.
@@ -106,7 +121,8 @@ To upgrade python:
1. First, change the version installed in the bootstrap ports.
1. Run `bootstrap.sh` to install this version.
-1. Run `init.sh` to install ansible, etc., for the new python version.
+1. (Optional) Remove (or rename for backup purposes) the old virtual environment in `.virtualenv`.
+1. Run `init.sh -u` to create a new virtual environment using the new python version.
1. After verifying that things work with the new python version, optionally remove the old version.
diff --git a/bootstrap.sh b/bootstrap.sh
index 0cbfe85..f08886d 100755
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -4,8 +4,10 @@
SCRIPT="${(%):-%x}"
DIR="$( cd "$( dirname "${SCRIPT}" )" >/dev/null 2>&1 && pwd )"
+source "${DIR}/common.sh"
+
# Run bootstrap. Installs Xcode's "additional components", if necessary. Installs MacPorts and ports required by setup.
-"$(python3 -m site --user-base)"/bin/ansible-playbook "${DIR}"/bootstrap-playbook.yml "$@"
+_run_ansible_playbook "${DIR}" bootstrap-playbook.yml "$@"
# Local Variables:
diff --git a/common.sh b/common.sh
new file mode 100644
index 0000000..9839b27
--- /dev/null
+++ b/common.sh
@@ -0,0 +1,47 @@
+# Common shell functions and variables.
+
+function _ansible_venv_() {
+ local script_dir="${1}"
+ shift || return 1
+ echo "${script_dir}/.virtualenv"
+}
+
+function _ansible_venv_exe_() {
+ local executable="${1}"
+ local script_dir="${2}"
+ shift || return 2
+ echo "$(_ansible_venv_ "${script_dir}")"/bin/"${executable}"
+}
+
+function _ansible_pip_() {
+ _ansible_venv_exe_ pip3 "$@"
+}
+
+function _ansible_galaxy_() {
+ _ansible_venv_exe_ ansible-galaxy "$@"
+}
+
+function _ansible_playbook_() {
+ _ansible_venv_exe_ ansible-playbook "$@"
+}
+
+function _ansible_home_() {
+ local script_dir="${1}"
+ shift || return 1
+
+ echo "${script_dir}/.ansible"
+}
+
+function _run_ansible_playbook_() {
+ local script_dir="${1}"
+ local playbook="${2}"
+ shift 2 || return 1
+
+ local cmd
+ cmd="$(_ansible_playbook_ "${script_dir}")"
+
+ ANSIBLE_HOME="$(_ansible_home_ "${script_dir}")"
+ export ANSIBLE_HOME
+
+ "${cmd}" "${script_dir}/${playbook}" "$@"
+}
diff --git a/dev-init.sh b/dev-init.sh
index 909bd5b..14d304f 100755
--- a/dev-init.sh
+++ b/dev-init.sh
@@ -6,7 +6,11 @@
SCRIPT="${(%):-%x}"
DIR="$( cd "$( dirname "${SCRIPT}" )" >/dev/null 2>&1 && pwd )"
-pip3 install --user --upgrade -r "${DIR}"/dev-requirements.txt --no-warn-script-location
+source "${DIR}/common.sh"
+
+PIP="$(_ansible_pip_ "${DIR}")"
+
+"${PIP}" install --upgrade -r "${DIR}"/dev-requirements.txt --no-warn-script-location
# Local Variables:
diff --git a/init.sh b/init.sh
index ba697b0..52764dc 100755
--- a/init.sh
+++ b/init.sh
@@ -4,6 +4,9 @@
SCRIPT="${(%):-%x}"
DIR="$( cd "$( dirname "${SCRIPT}" )" >/dev/null 2>&1 && pwd )"
+zparseopts -D -E -F -- \
+ u=UPGRADE -upgrade-venv=UPGRADE
+
# Accept the Xcode license, install python and ansible-galaxy dependencies, including ansible itself.
# Can be run again to upgrade any of the python or ansible-galaxy dependencies.
@@ -16,15 +19,37 @@ if ! xcodebuild -checkFirstLaunchStatus; then
echo "Done."
fi
+source "${DIR}/common.sh"
+
+VENV="$(_ansible_venv_ "${DIR}")"
+PIP="$(_ansible_pip_ "${DIR}")"
+GALAXY="$(_ansible_galaxy_ "${DIR}")"
+
+ANSIBLE_HOME="$(_ansible_home "${DIR}")"
+export ANSIBLE_HOME
+
+if [[ ! -e "${PIP}" ]]; then
+ # Create the virtual environment. This will use whatever version of python is on the path, and will copy
+ # that python and its libraries into the virtualenv rather than linking them.
+ python3 -m venv --copies --without-scm-ignore-files "${VENV}"
+elif (( $#UPGRADE > 0 )); then
+ echo "Upgrading virtual environment..."
+ # TODO Annoyingly, this prints an error if the python version is the same in the path and virtualenv...
+ python3 -m venv --copies --upgrade --without-scm-ignore-files "${VENV}"
+ echo "Done."
+else
+ echo "Skipping virtual environment creation/upgrade."
+fi
+
# Install the latest version of pip. Some older versions won't download cryptography wheels for some reason, causing
# the ansible install to fail.
-pip3 install --user --upgrade pip --no-warn-script-location
+"${PIP}" install --upgrade pip --no-warn-script-location
# Install ansible and python requirements needed by tasks used in these playbooks.
-pip3 install --user --upgrade -r "${DIR}"/requirements.txt --no-warn-script-location
+"${PIP}" install --upgrade -r "${DIR}"/requirements.txt --no-warn-script-location
# Install playbook requirements from ansible galaxy.
-"$(python3 -m site --user-base)"/bin/ansible-galaxy install -r "${DIR}"/requirements.yml
+"${GALAXY}" install -r "${DIR}"/requirements.yml
# Local Variables:
diff --git a/requirements.txt b/requirements.txt
index 8d0a928..f2dca86 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
# Python requirements for ansible tasks used in this playbook. Will be installed by init.sh
-ansible # Obviously.
+ansible # Obviously.
macholib # Dependency of `emacs` role. Used to make emacs standalone.
diff --git a/setup.sh b/setup.sh
index d26e9f7..5a156f9 100755
--- a/setup.sh
+++ b/setup.sh
@@ -11,8 +11,10 @@ DIR="$( cd "$( dirname "${SCRIPT}" )" >/dev/null 2>&1 && pwd )"
# defined here.
export PATH=/opt/local/bin:/opt/local/sbin:"${PATH}"
-# Assumes ansible was installed with pip3 --user (see init.sh).
-"$(python3 -m site --user-base)"/bin/ansible-playbook "${DIR}"/setup-playbook.yml "$@"
+source "${DIR}/common.sh"
+
+# Run the setup playbook with any provided arguments.
+_run_ansible_playbook_ "${DIR}" setup-playbook.yml "$@"
# Local Variables:
diff --git a/sudoers.sh b/sudoers.sh
index c6aff27..6399970 100755
--- a/sudoers.sh
+++ b/sudoers.sh
@@ -7,8 +7,10 @@
SCRIPT="${(%):-%x}"
DIR="$( cd "$( dirname "${SCRIPT}" )" >/dev/null 2>&1 && pwd )"
-# Assumes ansible was installed with pip3 --user (see init.sh).
-"$(python3 -m site --user-base)"/bin/ansible-playbook "${DIR}"/sudoers-playbook.yml "$@"
+source "${DIR}/common.sh"
+
+# Run the sudoers playbook with any provided arguments.
+_run_ansible_playbook_ "${DIR}" sudoers-playbook.yml "$@"
# Local Variables: