gh-XcodesOrg-XcodesApp/DECISIONS.md

8.3 KiB

Decisions

This file exists to provide a historical record of the motivation for important technical decisions in the project. It's inspired by Architectural Decision Records, but the implementation is intentionally simpler than usual. When a new decision is made, append it to the end of the file with a header. Decisions can be changed later. This is a reflection of real life, not a contract that has to be followed.

Why Make Xcodes.app?

xcodes has been well-received within and outside of Robots and Pencils as an easy way to manage Xcode versions. A command line tool can have a familiar interface for developers, and is also easier to automate than most GUI apps.

Not everyone wants to use a command line tool though, and there's an opportunity to create an even better developer experience with an app. This is also an opportunity for contributors to get more familiar with SwiftUI and Combine on macOS.

Code Organization

To begin, we will intentionally not attempt to share code between xcodes and Xcodes.app. In the future, once we have a better idea of the two tools' functionality, we can revisit this decision. An example of code that could be shared are the two AppleAPI libraries which will likely be very similar.

While the intent of xcodes' XcodesKit library was to potentially reuse it in a GUI context, it still makes a lot of assumptions about how the UI works that would prevent that happening immediately. As we reuse that code (by copying and pasting) and tweak it to work in Xcodes.app, we may end up with something that can work in both contexts.

Asynchrony

Xcodes.app uses Combine to model asynchronous work. This is different than xcodes, which uses PromiseKit because it began prior to Combine's existence. This means that there is a migration of the existing code that has to happen, but the result is easier to use with a SwiftUI app.

Dependency Injection

xcodes used Point Free's Environment type, and I'm happy with how that turned out. It looks a lot simpler to implement and grow with a codebase, but still allows setting up test double for tests.

State Management

While I'm curious and eager to try Point Free's Composable Architecture, I'm going to avoid it at first in favour of a simpler AppState ObservableObject. My motivation for this is to try to have something more familiar to a contributor that was also new to SwiftUI, so that the codebase doesn't have too many new or unfamiliar things. If we run into performance or correctness issues in the future I think TCA should be a candidate to reconsider.

Privilege Escalation

Unlike xcodes, there is a better option than running sudo in a Process when we need to escalate privileges in Xcodes.app, namely a privileged helper.

A separate, bundle executable is installed as a privileged helper using SMJobBless and communicates with the main app (the client) over XPC. This helper performs the post-install and xcode-select tasks that would require sudo from the command line. The helper and main app validate each other's bundle ID, version and code signing certificate chain. Validation of the connection is done using the private audit token API. An alternative is to validate the code signature of the client based on the PID from a first "handshake" message. DTS seems to say that this would also be safe against an attacker PID-wrapping. Because the SMJobBless + XPC examples I found online all use the audit token instead, I decided to go with that. The tradeoff is that this is private API.

Uninstallation is not provided yet. I had this partially implemented (one attempt was based on DoNotDisturb's approach) but an issue that I kept hitting was that despite the helper not being installed or running I was able to get a remote object proxy over the connection. Adding a timeout to getVersion might be sufficient as a workaround, as it should return the string immediately.

Selecting the active version of Xcode

This isn't a technical decision, but we spent enough time talking about this that it's probably worth sharing. When a user has more than one version of Xcode installed, a specific version of the developer tools can be selected with the xcode-select tool. The selected version of tools like xcodebuild or xcrun will be used unless the DEVELOPER_DIR environment variable has been set to a different path. You can read more about this in the xcode-select man pages. Notably, the man pages and some notarization documentation use the term "active" to indicate the Xcode version that's been selected. This older tech note uses the term "default". And of course, the xcode-select tool has the term "select" in its name. xcodes used the terms "select" and "selected" for this functionality, intending to match the xcode-select tool.

Here are the descriptions of these terms from Apple's Style Guide:

active: Use to refer to the app or window currently being used. Preferred to in front. default: OK to use to describe the state of settings before the user changes them. See also preset preset: Use to refer to a group of customized settings an app provides or the user saves for reuse. select: Use select, not choose, to refer to the action users perform when they select among multiple objects

Xcodes.app has this same functionality as xcodes, which still uses xcode-select under the hood, but because the main UI is a list of selectable rows, there may be some ambiguity about the meaning of "selected". "Default" has a less clear connection to xcode-select's name, but does accurately describe the behaviour that results. In Xcode 11 Launch Services also uses the selected Xcode version when opening a (GUI) developer tool bundled with Xcode, like Instruments. We could also try to follow Apple's lead by using the term "active" from the xcode-select man pages and notarization documentation. According to the style guide "active" already has a clear meaning in a GUI context.

Ultimately, we've decided to align with Apple's usage of "active" and "make active" in this specific context, despite possible confusion with the definition in the style guide.