DotEnvy

I released a new Swift library, DotEnvy. It's a parser and loader for dotenv files.

Dotenv is a vaguely specified format that is supported by libraries found for most languages used in server-side development. The idea is that a twelve-factor app is supposed to read its configuration from environment variables, which can be a hassle to maintain during development. So you store them in a non-version controlled file called .env your application reads upon startup.

The format looks more or less like this:

KEY1=VALUE1
KEY2="VALUE2"
KEY3="MULTILINE
VALUE"
KEY4='REFERENCE TO ${KEY3}'

The Swift libraries I could find seemed to lack features and had not seen updates in years. I don't think a library like this needs a huge number of features, but multiline strings and variable references were something I wanted. And writing parsers is fun.

DotEnvy has good test coverage and online documentation. There's also pretty good error reporting.

There's also a command line tool that allows you to syntax check a dotenv file or convert it to JSON. I was going add a launcher (i.e. run dotenv-tool launch sh and it'd export the environment from .env and run sh), but discovered that pseudo terminals are a pain and my Stevens has gone missing. Patches are welcome.

I accidentally used the same name as a Rust dotenv library, but I decided there's enough namespacing provided by the language support that the risk of confusion isn't too great.

Git history search with fzf

fzf is one of my favorite shell tools. I have a ton of scripts where I use it for selection. Here's one for searching git history. git log -Gpattern allows you to search for commits that contain pattern in the patch text. Combine it with fzf and you get a pretty decent history search tool.

I have this saved as ~/bin/git-search-log, so I can invoke it as git search-log pattern or git search-log pattern branch:

#!/bin/bash

set -euo pipefail

# Ensure compatibility with fish etc by ensuring fzf uses bash for the preview command
export SHELL=/bin/bash

git log -G$@ --oneline | fzf \
    --preview-window=bottom,80% \
    --preview "echo {} | sed 's/ .*//g' | xargs git show --color" \
    --bind 'enter:execute(commit=$(echo {} | sed "s/ .*//g") && git diff-tree --no-commit-id --name-only $commit -r | fzf --preview-window=bottom,80% --preview "git show --color $commit -- $(git rev-parse --show-toplevel)/\{}")'

When you run it you get an selection of matching commits, one per line, with a preview window showing the patch. If you hit enter on a commit, you get another fzf screen, this time allowing you to select files modified in that commit. Hit enter again and you're back in the first one.

Splitting a Xcode project with SPM

I was talking on Mastodon about splitting an Xcode project into smaller pieces. Here's an elaboration.

Background: With SwiftUI previews you want small focused Xcode schemes. The larger your scheme, the less likely the preview is to succeed. You also get faster compilation if you don't always build the whole app. You can get smaller schemes with framework build targets or with Swift Package Manager. Frameworks are a pain in the ass, SPM mostly isn't.

A note about the confusing terminology, in case you're not familiar with both Xcode and SPM: Xcode has projects, targets and schemes. SPM has products and targets. SPM products show up in the scheme selector of Xcode.

Here's how you set up my ideal Xcode app project:

Create a root directory for your project. This is where you have your .git folder or equivalent. In the root directory, create a new Xcode project, so you have root/MyApp/MyApp.xcodeproj and next to it root/MyApp/MyApp/ for the app files.

Then create a SPM package directory next to MyApp, something like root/MyAppCore/. Run swift package init --type library in it. Now open root in Finder and drag MyAppCore into MyApp in the Xcode Project Navigator sidebar (you don't need a workspace if you drop it onto the project.) After this you should have two entries under the MyApp project in Xcode: MyAppCore and MyApp. You should also see MyAppCore in the Xcode scheme selector.

After this you only have to add MyAppCore to the app build target in the Frameworks, Libraries and Embedded Content section of the project editor's General tab, and you're good to go.

With a setup like this, you can keep very little code on the app side, and put almost everything in the SPM package. You'll probably need your SwiftUI App, app delegate or scene delegate, depending on what and how you're building, on the app side, but other than that, just shove everything into the package. Every file you add to the package instead of the app is one change to the project file you avoided.

Ideally you want a wide but shallow SPM project structure with as many targets as is necessary, but structured so that they don't build up into deep dependency trees. You don't need to create a SPM product for each target; it's enough to create a projects as you need them from previews or for when you want to have Xcode compile just one part of the package.

To make imports easier in the app, you can use the MyAppCore product as an umbrella module. That means the MyAppCore target depends on every other target in the package, and then in MyAppCore/MyAppCore.swift calls @_exported import on all of them. @_exported is an underscored attribute and thus may break at some point, but it doesn't seem like a huge risk. So in Package.swift you can have this:

.target(
    name: "MyAppCore",
    dependencies: [
        "DB",
        "Networking",
        "PreferencesUI",
        /* all the other targets */
    ]
),

and then in MyAppCore.swift:

@_exported import DB
@_exported import Networking
@_exported import PreferencesUI
/* all the other targets */

Now when you do import MyAppCore on the app side, you get everything.

With a setup like you rarely need to touch the Xcode project file. It reduces the need for a tool like XcodeGen, which is priceless on a more monolithic project. If you want to avoid the project editor completely, I recommend looking into it.

© Juri Pakaste 2024