xcodes for xcode switching

As part of digging through my various problems with Xcode 14.3 (Feedback FB12154691, FB12154887, and some test case issues involving floating point math), I needed to install Xcode 14.2 to move my buildfarm backwards. Although this didn't enitrely fix the problem, it was an essential element of the debugging and remediation.

For manual work, there's a great list of Xcode releases available with direct links to Apple's downloads and release notes.

Since I was in the need to do this across five different machines, automation was on my mind, so I looked for the latest in tooling to help with this install process.

The latest in this field is xcodes, open source tooling for installing one or more copies of xcode and switching between them.

xcodes Commands

  • Use xcodes installed to find out which versions are installed
  • Use xcodes install XX.YY to install a specific version
  • Use xcodes select XX.YY to make the specified version the default
  • Use xcodes uninstall XX.YY to uninstall a specific version

Minimizing traffic and logins

To retrieve the xcode installer from Apple, you need to be logged in to a developer account, and that means credentials. Coordinating that with automation is painful and also would result in pulling every installer I need (each time I need it) across the internet.

As I'll be automating installation on multiple systems, I decided that I'd cache the items that I need in order to save time and bandwidth.

To download the packages into your cache:

xcodes download --directory CACHE_DIR 13.2.1

Automating installation

For installation (via Ansible), I'm using the following (assuming item.version contains the version on input):

- name: copy xcode installer
    src: "{{ xcode_cache }}/{{ item.package }}"
    dest: "{{ root_home }}/Downloads/{{ item.package }}"
    owner: "{{ owner }}"
    group: "{{ group }}"
    mode: '0644'

- name: "install xcode {{ item.version }}"
    cmd: "xcodes install --experimental-unxip --path {{ root_home }}/Downloads/{{ item.package }} {{ item.version }}"
  become: true

- name: remove installer
    path: "{{ root_home }}/.Trash/{{ item.package }}"
    state: absent
  ignore_errors: true

This uses xcodes to install without downloading, based on the xip file (along with enabling the experimental fast unxip code).

This code is called using include_tasks from a loop in my main ansible ci-bot file that installs appropriate versions:

- name: determine current xcodes
      cmd: "xcodes installed | cut -f1 -d' '"
    register: xcodes_installed

- name: install xcode if missing
    include_tasks: ci-xcode.yml
    loop: "{{ xcode_versions }}"
      - item.version not in xcodes_installed.stdout_lines
      - ansible_distribution_version >= item.min_os
      - item.max_os is not defined or (ansible_distribution_version < item.max_os )

The first task gets a list of current xcodes (to keep from reinstalling) and then installs only if it's not already installed and the version is appropriate for the version of macOS that we're installing on.

xcode_versions looks like this:

  - version: '13.2.1'
    package: 'Xcode-13.2.1+13C100.xip'
    min_os: '11.3.0'
    max_os: '12.0.0'
  - version: '14.3'
    package: 'Xcode-14.3.0+14E222b.xip'
    min_os: '13.0.0'
# intentionally out-of-order because 14.2 is preferred right now
  - version: '14.2'
    package: 'Xcode-14.2.0+14C18.xip'
    min_os: '12.5.0'