May 2, 2021
You may know you can edit and build your Swift PM projects outside of Xcode, but what about visual debugging? The Debugger Adapter Protocol (DAP), one of the many gifts to the world from the VSCode project, allows us to hook up our editor of choice (not just VSCode) to LLDB to visually debug our Swift code.
You still need to do this on a Mac with Xcode installed. Theoretically this process should work on any machine that can build the llvm-project but right now, without the
LLDB.framework that ships with Xcode, you'll be unable to inspect variables at stopped breakpoints.
This is a pretty big deal for me but I'm hoping that whatever magic sauce is in the Xcode-packaged
LLDB.framework makes its way up to the mainline llvm-project eventually2.
If you still need a Mac and Xcode, then why not just use Xcode? Excellent question. This process at least gives you the choice of what editor to use, and enables the full development cycle outside of Xcode (yes, yes and AppCode).
In this post we're going to:
routes.swiftfile of the Hello World project and confirm that we can run, break, and see the call stack and local variables.
Phew. Let's get started.
I'm going to assume you already have lsp-mode installed. Make sure you have
(require 'lsp-mode) and
(require 'lsp-ui) somewhere in your Emacs config.
For dap-mode, follow the steps on the dap-mode Github README. I keep it pretty simple with:
(require 'dap-lldb) (dap-auto-configure-mode)
dap-mode uses debug templates. You'll eventually want to add one for every target you debug. For now, add this to your config for our Hello World example.
(dap-register-debug-template "Vapor Hello World SwiftPM" (list :type "lldb-vscode" :cwd "/come/back/and/fill/this/in/later" :request "launch" :program "/come/back/and/fill/this/in/later/Run" :name "Run"))
Don't worry about the missing paths for now... we'll come back and, well you get the idea.
You'll need CMake and the Ninja build system installed.
brew install cmake ninja
Open your terminal, start brewing a coffee (you'll need it shortly) and type:
git clone https://github.com/llvm/llvm-project cd llvm-project git checkout llvmorg-12.0.0
I'm using the latest release tag as of this writing, llvmorg-12.0.0, but you can check the current release3.
This next step is pretty weird but here we are. You need to completely remove the Xcode command-line tools in order to prevent build errors for llvm-project. We'll be reinstalling them shortly, but for now quit Xcode if it's running and, in terminal, type:
sudo xcode-select -s /Applications/Xcode.app sudo rm -rf /Library/Developer/CommandLineTools
Ok let's build llvm-project. If you're not on Apple Silicon skip the line with
rm -rf build mkdir build cd build cmake -G Ninja \ -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;lldb" \ -DCMAKE_OSX_ARCHITECTURES='arm64' \ -DCMAKE_C_COMPILER=`which clang` \ -DCMAKE_CXX_COMPILER=`which clang++` \ -DCMAKE_BUILD_TYPE=Release \ -DLLDB_INCLUDE_TESTS=OFF \ -DLLDB_BUILD_FRAMEWORK=ON \ -DDEFAULT_SYSROOT="$(xcrun --show-sdk-path)" \ ../llvm cmake --build . -- -j8
Remember that coffee? This is where you get to sip it as you watch llvm build. No rush.
Assuming that the build succeeded:
sudo xcode-select --install
Part of the llvm-project that we built was the
lldb-vscode extension (not to be confused with the completely unrelated vscode-lldb project). We need to copy the binary into a place where
dap-mode will find it:
mkdir -p ~/.vscode/extensions/llvm-org.lldb-vscode-0.1.0/bin cp ../lldb/tools/lldb-vscode/package.json ~/.vscode/extensions/llvm-org.lldb-vscode-0.1.0 cp bin/lldb-vscode ~/.vscode/extensions/llvm-org.lldb-vscode-0.1.0/bin
The rpath of the lldb-vscode extension we just built points directly into the
bin directory of llvm-project tree, which is where it's going to look for
LLDB.framework. You can confirm that by typing:
cd ~/.vscode/extensions/llvm-org.lldb-vscode-0.1.0/bin otool -l lldb-vscode
Look for the
LC_RPATH entry. We need to change it, because we actually want lldb-vscode to use the
LLDB.framework from your currently installed Xcode (12.5 at the time of this writing). The
LLDB.framework we just built as part of llvm-project won't fully work: you won't be able to see locals or a stack trace during debugging, which makes it less useful.
Here's how to replace the rpath. You'll have to plug in the current rpath from the
otool command above.
cd ~/.vscode/extensions/llvm-org.lldb-vscode-0.1.0/bin install_name_tool -delete_rpath /exact/rpath/from/the/otool/command/you/just/issued lldb-vscode install_name_tool -add_rpath /Applications/Xcode.app/Contents/SharedFrameworks lldb-vscode
Check again with
otool -l lldb-vscode. There should be only one LC_RPATH, and it should point to
You can try this with any Swift PM project of course but Vapor's Hello World is a perfect example. Change into your Documents/Projects/Scribbling directory (mine is
/Users/gene/Proj). Install Vapor with:
brew tap vapor/tap brew install vapor/tap/vapor vapor --help
If that worked you'll see a list of available vapor commands. Ok let's create the Hello World app.
vapor new Hello cd Hello swift build find . -name Run
That last command will show you where the Run binary got dropped. Make a note of the full path to the file ... on my machine it's
Go back to your Emacs config and modify the debug template with the correct working directory and program to run. Mine is:
(dap-register-debug-template "Vapor Hello World SwiftPM" (list :type "lldb-vscode" :cwd "/Users/gene/Proj/Hello/.build/arm64-apple-macosx/debug" :request "launch" :program "/Users/gene/Proj/Hello/.build/arm64-apple-macosx/debug/Run" :name "Run"))
Reload your Emacs config.
Start debugging with
M-x dap-debug. Select the template you just added: that should launch the Vapor web server and the Hello World app.
In Emacs, find
Sources/App/routes.swift in the Hello World project. Move the point to the line that says
return "It works!" (line 5 for me). Issue
M-x dap-breakpoint-add. You should see a breakpoint indicator dot appear to the left of the line.
Open a web browser and navigate to
http://127.0.0.1:8080. Your breakpoint should hit and you'll see something like this.
Under Locals (top-right on my screen), if you click on
req it will expand and show you the Vapor request properties at the time your breakpoint was hit. If you don't see
req at all, something went wrong and lldb-vscode isn't using an
LLDB.framework that can show locals. My only recommendation is to try this procedure again to see if you missed anything. It's also possible that this functionality will regress again in the future (like it did in LLVM 11.0.1 / Xcode 12.4) so join me in keeping our fingers crossed.
The stack trace is available by clicking the
Run label in the
Debug Sessions pane. To stop debugging issue
M-x dap-disconnect. I'll leave further investigation to you, and I may also expand on this in future blog posts.
Congratulations on making it this far, and on getting it to work. My hope is that the LLVM and DAP story will continue to expand and improve in the future, possibly dropping the dependency on Xcode entirely.
1 I am not familiar with the VSCode extensions ecosystem but I imagine it would be even more straightforward to set up there.
2 At the recommendation of one of the reviewers of this post (thanks Dan!) I tried this process using Apple's fork of the llvm-project but still could not shake the dependency on Xcode's LLDB.framework.
3 It's worth mentioning that llvm 12.0.0 and Xcode 12.5 work, whereas llvm 11.0.1 and Xcode 12.4 did not (no locals or call stack during debugging). Hopefully whatever got fixed stays fixed.