cquery and nix

2018-01-06
Updated 2018-01-07

cquery is a language server for C, C++, and Objective-C built on libclang. I use it in emacs with the help of lsp-mode and lsp-ui. The wrinkle is that I define my development environments using nix. This article shows a sample project using these things along with some work-in-progress glue I've written.

A cquery Nix Derivation

I have not submitted a cquery package to nixpkgs yet as cquery is not yet tagging releases, and things are very much in flux. My latest packaging is updated every so often when I notice what looks like an interesting change to cquery.

Sample Project

Let's create a development environment in which we can use cmake to build things, pkg-config to find dependencies, opencv3 as a dependency, and cquery. Here is the shell.nix,

with (import <nixpkgs> {});
stdenv.mkDerivation {
  name = "cquery-test";
  buildInputs = [ cmake pkgconfig opencv3 cquery ];
}

We create a CMakeLists.txt so cmake can compile our program.

cmake_minimum_required (VERSION 3.1)
project (cquery-test)
find_package(PkgConfig)
pkg_check_modules(OPENCV REQUIRED opencv)

set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-missing-braces -std=c++1z")

add_executable(woo src/main.cpp)
string(REPLACE ";" " " OPENCV_LDFLAGS "${OPENCV_LDFLAGS}")
target_link_libraries(woo ${OPENCV_LDFLAGS})
target_include_directories(woo PUBLIC ${OPENCV_INCLUDE_DIRS})
target_compile_options(woo PUBLIC ${OPENCV_CFLAGS_OTHER})

Letting cquery Know What clang Knows

A preferred way to use cquery is through a compile_commands.json file that records how the compiler is invoked to compile each source file. We instruct cmake to produce such a file for us,

cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1

Part of the trouble is that nix provides us with a wrapper around clang that knows to check the environment to find additional include directories. This means that the compiler invocation cmake records does not explicitly mention every include directory cquery will need to simulate the compiler using libclang.

Since we included cquery in our shell.nix, we will get a bonus bash function to help us out. Here is some command line action,

$ nix-shell

[nix-shell]$ nix-cquery
Adding Nix include directories to the compiler commands
There is no compile_commands.json file to edit!
Create one with cmake using:
(cd build && cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 .. && mv compile_commands.json ..)

We entered our nix-shell and called the nix-cquery function, it tells us how to create a compile_commands.json file since we don't yet have one.

[nix-shell]$ mkdir build

[nix-shell]$ (cd build && cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 .. && mv compile_commands.json ..)
-- The C compiler identification is Clang 4.0.1
-- The CXX compiler identification is Clang 4.0.1
-- Check for working C compiler: /nix/store/61plv8rx9jvjzk8awp7cxv42gy4g41a6-clang-wrapper-4.0.1/bin/clang
-- Check for working C compiler: /nix/store/61plv8rx9jvjzk8awp7cxv42gy4g41a6-clang-wrapper-4.0.1/bin/clang -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /nix/store/61plv8rx9jvjzk8awp7cxv42gy4g41a6-clang-wrapper-4.0.1/bin/clang++
-- Check for working CXX compiler: /nix/store/61plv8rx9jvjzk8awp7cxv42gy4g41a6-clang-wrapper-4.0.1/bin/clang++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found PkgConfig: /nix/store/w4zq8xn7qldlf68f14200nx8zshlr5lw-pkg-config-0.29.2/bin/pkg-config (found version "0.29.2")
-- Checking for module 'opencv'
--   Found opencv, version 3.3.0
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/acowley/temp/cquery/hooja/build

[nix-shell]$ nix-cquery
Adding Nix include directories to the compiler commands

And that should do it for the environment! Let's take a look at our project's source code.

Emacs

I use an elisp function to prepare the right modes when editing a C++ source file.

(defun cquery-nix-shell ()
  "Find a cquery executable in a nix-shell associated with the
directory containig the current file if that file’s extension is
`cpp` or `hpp`. Use the location of that executable in the nix
store to load and configure the cquery lsp client."
  (when (let ((ext (file-name-extension (or (buffer-file-name) ""))))
          (and (not (null ext))
               (or (string-equal ext "cpp")
                   (string-equal ext "hpp"))))
    (let ((dir (find-nix-shell)))
      (when dir
        (let* ((cquery-exe
                (string-trim
                 (shell-command-to-string
                  (concat "nix-shell " dir
                          " --run 'which cquery'"))))
               (cquery-root (file-name-directory
                             (directory-file-name
                              (file-name-directory cquery-exe)))))
          (require 'cquery
                   (concat cquery-root
                           "share/emacs/site-lisp/cquery.el"))

          (setq-local cquery-executable cquery-exe)
          (require 'lsp-flycheck)
          (flycheck-mode)
          (lsp-cquery-enable)
          (helm-gtags-mode -1)
          (local-set-key (kbd "M-.") #'xref-find-definitions))))))

And we can finally look at some source code,

#include <iostream>
#include <opencv2/opencv.hpp>

int main() {
  using namespace std;
  cout << "Displaying webcam video" << endl;

  auto cap = cv::VideoCapture(0);
  if(!cap.isOpened()) {
    cerr << "Couldn't open camera" << endl;
    return 1;
  }
  cv::namedWindow("Cam Demo");
  while(cv::waitKey(30) < 0) {
    cv::Mat frame;
    cap >> frame;
    imshow("Cam Demo", frame);
  }

  return 0;
}

But inert source code is no fun; what can cquery tell us after we evaluate (cquery-nix-shell) in that buffer?

cquery-emacs.png

Sure we would get error and warning highlights if any applied, but here I'm showing what we see with some perfectly cromulent code: extra semantic information! The cursor (point in emacs lingo) is on the cap >> frame; line. Since it is on cap, we see other occurrences of that identifier highlighted. We see the type of that identifier (cv::VideoCapture) in the echo area at the bottom of the frame, and we see the type of every identifier on the current line as a margin note on the right hand side of the window. Awesome!