šš»āāļø Note: A version of this post also appears at the Codecov Blog and The New Stack.
At my job, I focus on developer automation and tooling for mobile apps. One of the newer tools weāve been using is Codecov. It tracks and reports on our code coverage in total, and on pull requests.
One of my recent focuses has been finding ways to optimize our CI workflows. I noticed that for our iOS app, the Codecov step of our workflow was taking much longer than expected, so I decided to find out why, and if there was anything we could do to improve it.
Xcode collects code coverage data and can display it in the IDE for developers. It can also be exported as an .xcresult
file when using xcodebuild
from the command line, like so:
xcrun xcodebuild test \
-project tconnect.xcodeproj \
-scheme tconnect \
-testPlan tconnect \
-destination "platform=iOS Simulator,name=iPhone 14" \
-derivedDataPath DerivedData \
-resultBundlePath artifacts/ResultBundle.xcresult
These xcresult files are great, and can be useful in lots of different ways1 , but like many things Apple, they can be difficult to use outside the Apple ecosystem.
xcresult files are a binary format, and while they can be converted into a JSON representation using the xccov
binary included with Xcode, the resulting JSON is not in one of the standard coverage formats that Codecov can ingest. The format has also been known to change without warning with new Xcode releases.
So in order for Codecov to use the coverage results from Xcode, they have to be converted into another format. Codecovās official GitHub Action can do the conversion, but the way it handles this conversion is by analyzing the coverage for each file one by one, which can take up to a second for each file. This is a fine enough approach for some projects, but when working with a large codebase like ours, that can take quite some time.
xcresultparser
xcresultparser
is an open source Swift tool that can parse .xcresult
files, and convert them into various other formats. One of these formats is Cobertura XML, which Codecov natively supports.
The big advantage xcresultparser brings is, because it is a compiled program and not a script, it can utilize multiple threads to do the conversion. This speeds up the conversion process immensely.
After running the xcodebuild
command above to generate the .xcresult
file, we tell xcresultparser to convert it like so:
xcresultparser \
--output-format cobertura \
"artifacts/ResultBundle.xcresult" >"artifacts/coverage.xml"
And finally, we tell the Codecov GitHub Action to upload that XML file instead of the xcresult file.
So, just how much time savings are we getting?
Ā | Total Build Time Before | Total Build Time After | Delta |
---|---|---|---|
App Unit Tests | 18m 9s | 16m 12s | 1m 57s |
Library Unit Tests | 22m 8s | 15m 16s | 6m 52s |
We run these builds in parallel, so the total real-time savings for each build is the delta of the Library Unit Tests build; around 7 minutes! This might not seem like much, but when you factor in that weāre running these builds upwards of 20 times a day, itās a considerable time (and cost) savings. Thatās over 2 hours of total developer time saved per day; almost 12 hours per week!
While implementing xcresulparser for our project, I learned that it can also print a summary of test results to the command line. Our Library unit tests are 8 separate test suites that run in serial. If a test fails near the top of the log output, it can be difficult to find.
So, at the end of each test run, we print out a summary like so:
xcresultparser \
--output-format cli \
--failed-tests-only \
"${PACKAGES_RESULT_BUNDLE_PATH}"
This produces output that looks like this:
Test result summary example
xcresultparser has improved the lives of our developers quite a bit. And the fact that it is open source means that we as a developer community can help improve it for the benefit of ourselves and others. Check it out if youāre using Codecov (or another similar tool) to track code coverage on your Xcode projects.
Check out XCResultKit (which xcresultparser itself uses) if youād like to explore using xcresult files in other interesting waysĀ ↩
Being the father of three small children means that I have very little time for myself. From the hours of 6am to 8pm, my life is either my job or my family. This means that if I desire any time for myself, it has to be outside those hours.
Normally that means I stay up until midnight playing video games (Zelda lately) or watching TV (Star Trek or King of the Hill mostly), but Iāve found that on the vanishingly rare occasion Iām able to get myself up early, itās a far more enjoyable and productive time for me. The true challenge of course is actually getting out of the dang bed.
This morning, Iāve started the first of what Iām calling āGood Morningsā. The loose plan is to try to get up early every morning on Monday through Saturday (Sundayās are for sleeping in), and do something I want to do for myself. To start, Iām going to try using 3 mornings for something active, and 3 mornings for something chill/cognitive.
Active mornings will most likely be just going for walks outside. I love being outside in the dewy morning air, and my favorite place to walk is just a couple blocks from our house.
Chill mornings will be more varied, but will likely end up mostly taking place at my computer, at least at the start. I have a number of small computers tasks that have been piling up that Iād like to take care of (clean up my password manager database, prune my RSS feeds, that sort of thing). Iāve also got a couple coding projects that Iād like to dedicate some real time to (Sensorium mostly right now, and I dream of building a music library manager). Occasionally, Iād also like it to be a time for reading, watching TV or a movie, or playing a video game.
And so, this morning is the first of hopefully many Good Mornings. Itās 4:18 AM. Iām going to quickly finish this cup of coffee, and head out the door for a walk around Lake Nokomis. āļø
Iām not normally the type to post about this publicly, but Iām experimenting with the thought of it providing me some more motivation to stick with it maybe? Usually, it just adds to the shame of not sticking with the habit, which prevents me from doing it. That could happen here, but weāll see.
]]>Whenever re-running a job on GitHub Actions, there is a handy toggle for debug logging.
The āRe-run jobsā pop-up within GitHub Actions
Checking this prints out some extra information on the next run of the job, but by default is limited to information about GitHubās execution of the script, not about what happens during the actual run.
The way this toggle works is that it sets a secret named ACTIONS_STEP_DEBUG
to true
. GitHubās step execution process then reads this value, and prints out the extra information. Just like any secret in a GitHub Action though, we can also read this and use it to our liking.
Because this is a āsecretā and not a āvariableā, we have to do a tiny bit of work in order for our builds to have access to the value. Like any other secret, this can be done by reassigning the value of the secret to a variable manually in an env
block:
env:
ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG }}
Now any script or process run by the action can check the value of ACTIONS_STEP_DEBUG
, and use it to print out extra logging information.
I add the following to any Bash script that I use in my builds:
# Output extra debug logging if `TRACE` is set to `true`
# or if `ACTIONS_STEP_DEBUG` is set to `true` (GitHub Actions)
if [[ "${TRACE:-false}" == true || "${ACTIONS_STEP_DEBUG:-false}" == true ]]; then
set -o xtrace # Trace the execution of the script (debug)
fi
This allows us to read the value of that secret to print debug information on CI, as well as do so locally by setting TRACE=true
. Usually we just do this directly when we invoke the script.
TRACE=true ./scripts/do-stuff.sh
The above methods can be used in any language that supports reading of environment variables. For example, hereās how youād do it in Swift:
let environment = ProcessInfo.processInfo.environment
if environment["TRACE"] == "true" || environment["ACTIONS_STEP_DEBUG"] == "true" {
logger.debugMode = true
}
If you need to force an application on your Mac (and probably Linux too?) to use a specific shell on your system, it can be accomplished by setting the SHELL
environment variable.
This can be done either within a script:
#!/bin/bash
export SHELL=/bin/bash
open -a "/Applications/SuperCoolApp.app"
Or can be done right at the command line:
SHELL=/bin/bash open -a "/Applications/SuperCoolApp.app"
Bam! My thanks to my good friend Eli for the tip on this.
So why on earth would you want to do this? Letās say youāve got an application that runs shell commands as a part of its operation. Ideally, the app would explicitly target a specific shell to ensure compatibility across systems, but it also might just default to whatever your systemās login shell is. I ran into the latter case, and the commands being run by the application were failing because they were not compatible with my preferred shell, fish
. So now I just open the application using the shell command above, and voilĆ ! All works as it should.
Have you ever wondered how to detect if a macOS app youāre building was launched as a login item, as opposed to being launched manually by the user? Well, hereās how to do it in Swift:
private var launchedAsLogInItem: Bool {
guard let event = NSAppleEventManager.shared().currentAppleEvent else { return false }
return
event.eventID == kAEOpenApplication &&
event.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem
}
There are probably a number of reasons youād want to check for this. In my case, I wanted to prevent the main app window of my app CenterMouse from opening when the app was launched as a login item, but still open it if a user launches it manually.
You can see this code in context in CenterMouseās AppDelegate here: AppDelegate.swift.
]]>Iāve noticed a trend over the past 5 years or so of music being made that is inspired by what was on the radio when I was growing up and old enough to pay attention (late 90s to the 2010s). Itās a fun nostalgia trip, and most of the music Iāve heard in this realm is actually quite good.
Below are some good examples Iām familiar with in chronological order by release date. Please let me know if you know of any more!
Say the words āDerived Dataā to any Apple platform developer, and they will immediately cringe, remembering painful times of deleting the directory just to get Xcode to behave properly.
Iām here to tell you, there is a better way! (Or at least a slight improvement.)
Lately, Iāve taken to setting my Xcode projects to keep their derived data folders alongside the project itself. Xcode calls this a āProject-relative Locationā, and it can be set in the UI by navigating to āFileā > āProject Settingsā¦ā in the menu bar. Youāll be presented with a window, where you can change the āDerived Data:ā drop-down to āProject-relative Locationā.
Now whenever Xcode runs a build, it will place the derived data right next to your project. This has a number of advantages.
DerivedData/<project_name>/SourcePackages/
).The project settings window
This setting is a āper-userā setting though, which ideally should be included in your .gitignore
file, which means it will not get checked in and saved.
Previously, I would go manually set this setting on the projects that I worked on, but why use a mouse when you could use a script instead!
Hereās the Bash script I came up with to perform the modifications needed on whichever project or workspace you pass to it:
#!/usr/bin/env bash
# This script sets the user-specific Derived Data location setting
# for the given Xcode project or workspace to be "project-relative"
# and next to the project or workspace.
set -o errexit # Exit on error
set -o nounset # Exit on unset variable
set -o pipefail # Exit on pipe failure
# Output extra debug logging if `TRACE` is set to `true`
if [[ "${TRACE:-false}" == true ]]; then
set -o xtrace # Trace the execution of the script (debug)
fi
help() {
echo "Usage: $0 <path/to/project[.xcodeproj | .xcworkspace]>"
}
main() {
if [[ $# -ne 1 ]]; then
help
exit 1
fi
case "$1" in
*.xcodeproj) ;;
*.xcworkspace) ;;
-h | --help)
help
exit 0
;;
*)
help
exit 1
;;
esac
set_local_derived_data "$1"
}
set_local_derived_data() {
# Absolute path to the `.xcodeproj` or `.xcworkspace` file
local PROJECT_FILE=$1
if [[ ! -d "$PROJECT_FILE" ]]; then
echo "Error: $PROJECT_FILE does not exist or is not a directory"
exit 1
fi
# Absolute path to the current user's `xcuserdatad` directory
local XCUSERDATAD_DIR
if [[ "$PROJECT_FILE" == *".xcodeproj" ]]; then
XCUSERDATAD_DIR="${PROJECT_FILE}/project.xcworkspace/xcuserdata/$(whoami).xcuserdatad"
elif [[ "$PROJECT_FILE" == *".xcworkspace" ]]; then
XCUSERDATAD_DIR="${PROJECT_FILE}/xcuserdata/$(whoami).xcuserdatad"
fi
# Create the `xcuserdatad` directory if it doesn't exist
mkdir -p "$XCUSERDATAD_DIR"
WORKSPACE_SETTINGS_PLIST_PATH="${XCUSERDATAD_DIR}/WorkspaceSettings.xcsettings"
# Create the `WorkspaceSettings.xcsettings` file if it doesn't exist
if [[ ! -f "$WORKSPACE_SETTINGS_PLIST_PATH" ]]; then
plutil -create xml1 "$WORKSPACE_SETTINGS_PLIST_PATH"
fi
# Set the Derived Data settings
plutil -replace BuildLocationStyle -string UseAppPreferences "$WORKSPACE_SETTINGS_PLIST_PATH"
plutil -replace CustomBuildLocationType -string RelativeToDerivedData "$WORKSPACE_SETTINGS_PLIST_PATH"
plutil -replace DerivedDataCustomLocation -string DerivedData "$WORKSPACE_SETTINGS_PLIST_PATH"
plutil -replace DerivedDataLocationStyle -string WorkspaceRelativePath "$WORKSPACE_SETTINGS_PLIST_PATH"
# Validate the `WorkspaceSettings.xcsettings` file
plutil -lint "$WORKSPACE_SETTINGS_PLIST_PATH"
}
main "$@"
Simply call the script and pass it the path to an .xcodeproj
or .xcworkspace
, and it will do the modifications for you.
./set-local-derived-data.sh <path/to/project.xcodeproj>
Enjoy your newly local DerivedData directory!
]]>There were some great new releases this New Music Friday!
This is Panchikoās first true full-length, and itās surprisingly good for a band that hasnāt been active for over 25 years.
This is a remastered set of Glareās first 2 EPs, which Iād never heard before. Itās some excellent shoegaze.
Clearbody announced this new EP releasing in June, and dropped the first single. I loved their debut album One More Day, so Iām very excited for this one.
]]>Hereās the music that came out today that I was excited for, and a brief review of each one.
Hereās a little fish
function I whipped up to open GitHub in the web browser depending on the current state of the repo:
function gv --description 'Opens the current repository in the browser, trying first to open a pull request, then a branch, then the repository itself'
set -l current_branch (git symbolic-ref --short HEAD)
set -l pr_url (gh pr list --head "$current_branch" --state OPEN --json url --jq '.[0].url')
git rev-parse --abbrev-ref $current_branch@{upstream} >/dev/null 2>&1
set -l remote_branch_exists $status # 0 if the branch exists, something else if it doesn't
if test -n "$pr_url"
echo "Opening pull request $pr_url"
open $pr_url
else if test "$remote_branch_exists" -eq 0
and test "$current_branch" != main
and test "$current_branch" != master
echo "Opening branch '$current_branch'"
gh browse --branch $current_branch
else
echo "Opening repository"
gh browse
end
end
In summary:
I know fish
isnāt as popular as the venerable bash
, so hereās a version as a bash
script:
(I havenāt fully tested this version, so it may not work as expected.)
#!/bin/bash
CURRENT_BRANCH=$(git symbolic-ref --short HEAD)
PR_URL=$(gh pr list --head "${CURRENT_BRANCH}" --state OPEN --json url --jq '.[0].url')
git rev-parse --abbrev-ref "${CURRENT_BRANCH}@{upstream}" >/dev/null 2>&1
REMOTE_BRANCH_EXISTS=$? # 0 if the branch exists, something else if it doesn't
if [[ -n "${PR_URL}" ]]; then
echo "Opening pull request ${PR_URL}"
open "${PR_URL}"
elif [[ "$REMOTE_BRANCH_EXISTS" == 0 && "${CURRENT_BRANCH}" != main && "${CURRENT_BRANCH}" != master ]]; then
echo "Opening branch '${CURRENT_BRANCH}'"
gh browse --branch "${CURRENT_BRANCH}"
else
echo "Opening repository"
gh browse
fi