Übersicht


    Publish your Swift app via homebrew

    Motivation

    This is a recording of my own odyssey developing my first Swift command-line (CLI) app to change the default browser on macOS. For this tutorial I’ll ignore the fact that there is already a brew package that does this. So, how difficult can it be? Turns out, not that difficult, actually. The difficult part is to gather all information to build the complete »flow«.

    The use case: change the default browser in macOS.

    • Use Swift (with package manager)
    • Publish it via (private) homebrew

    Just a small note for the audience: I expect that you know how to do stuff on the command-line and you know at least one other programming language. If possible I drop links to additional material.

    Creating a package in Swift

    So here I am, no clue about software development for macOS. My first idea was to fire up Xcode and create a new project. This worked pretty well, I could write my code and run/debug it.

    However, I would now, after I’ve done everything, recommend a slightly different approach: Swift comes with a CLI tool, conveniently called swift to create new packages (related blog post). Surprise, surprise: I figured out that I have to create an app (and not a library). I called my app »defbro« (default browser), because »defaultbrowser« was already in use.

    mkdir defbro
    swift package init --type executable
    

    The big plus is that newer Xcode versions can just open Swift package repositories without running any additional commands. This gives us the boilerplate to create our default browser switcher. Let’s try it:

    swift build
    swift run
    

    Done. That seems to work. Now you can fire up Xcode and open the directory.

    Dependencies in Swift

    Creating a project with Xcode is straight forward, but the added benefit of Swift packages is easier to handle via the package description. For most CLI scripts, parsing command arguments is required. apple/swift-argument-parser seems nice, it is even supported by Apple.

    To use the argument parser, you have to adapt the Package.swift, resulting in something like this:

    let package = Package(
        name: "defbro",
        dependencies: [
            .package(url: "https://github.com/apple/swift-argument-parser", from: "0.4.0"),
        ],
        targets: [
            .target(
                name: "defbro",
                dependencies: [
                   .product(name: "ArgumentParser", package: "swift-argument-parser"),
                ]),
            .testTarget(
                name: "defbroTests",
                dependencies: ["defbro"]),
        ]
    )
    

    (full source)

    You can view the instructions at apple/swift-argument-parser and in the official swift docs.

    See also

    Notes on writing code

    With the basis set up, we can now dive into Swift basics. Haha, joke, I’m not diving into Swift here, there are plenty resources you can tap – let’s introduce some homebrew lingo already – into. Two references that helped me were

    I started off with the code from existing repositories, but found out that I do not want to write my code in some C-based language and that I do want a clear build, without deprecation warnings. In particular, I saw

    'LSCopyDefaultHandlerForURLScheme' was deprecated in macOS 10.15:
      Use LSCopyDefaultApplicationURLForURL() instead.
    'LSCopyAllHandlersForURLScheme' was deprecated in macOS 10.15:
      Use LSCopyApplicationURLsForURL() instead.
    

    in

    which led me to a rewrite. After a while, I successfully hacked the logic together, you can see the end result on github.

    Release on homebrew

    Homebrew is a totally different beast, but to wrap your head around it, you only need to know:

    • If you want to have your stuff published, you can either create a PR on the official core repo or you brew your own beer, eh, code.
    • To brew your own apps, you need a formula (aka package description) and a tap (aka git repo where you host your package).

    Formulae and Taps you can create with the brew command:

    A straight forward approach is to add a version tag with git tag v0.0.1 to the source code repo, git push --tags it to github and create a formula form the automatically generated tag tarball, an example you can find on the defbro repo.

    Copy the .tar.gz link and run brew create https://.../tags/v0.0.1.tar.gz with it. I ended up with

    class Defbro < Formula
      desc "Set the default browser from command-line"
      homepage "https://github.com/jwbargsten/defbro"
      url "https://github.com/jwbargsten/defbro/archive/refs/tags/v0.0.1.tar.gz"
      sha256 "90c5bfa02037cdcdb5356b935298d52d854f3ab1368bc4663b77e99e3287f8a6"
      license "MIT"
    
      depends_on xcode: [">= 11.2", :build]
      depends_on macos: [
        :catalina,
        :big_sur,
      ]
    
      def install
        system "swift", "build", "--disable-sandbox", "--configuration", "release"
        bin.install ".build/release/defbro"
      end
    
      test do
        system "true"
      end
    end
    

    In case of doubt, you can always clone homebrew-core and grep through the existing core formulae.

    See also