cquery and nix
2018-01-06Updated 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?
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!