Commit d686eca3 authored by Christian Hergert's avatar Christian Hergert

compile-commands: add IdeCompileCommands

This is a helper object to simplify the process of working with
clang-style compile_commands.json databases. Some build systems
such as Meson and CMake can benefit from having access to this
information in a unified manner.

Now that IdeBuildSystem can automatically translate paths from
the runtime build dir, this should allow us to remove some code
from those plugins (and share it in libide instead).

We might still want something to resolve things like -I includes
based on the relative working directory, but in practice, that
should match the $builddir of the build pipeline.
parent 39f89bbf
/* ide-compile-commands.c
*
* Copyright © 2017 Christian Hergert <chergert@redhat.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#define G_LOG_DOMAIN "ide-compile-commands"
#include <json-glib/json-glib.h>
#include "ide-debug.h"
#include "buildsystem/ide-compile-commands.h"
/**
* SECTION:ide-compile-commands
* @title: IdeCompileCommands
* @short_description: Integration with compile_commands.json
*
* The #IdeCompileCommands object provides a simplified interface to
* interact with compile_commands.json files which are generated by a
* number of build systems, including Clang tooling, Meson and CMake.
*
* Create a new #IdeCompileCommands instance, and then asynchronously
* load the file using ide_compile_commands_load_async(). After the
* database has been loaded, you can access build commands using
* ide_compile_commands_lookup().
*
* Due to the rather unfortunate design of JSON, this file holds on
* to a number of strings during the lifetime of the object, for each
* of the compile commands. On larger projects, this can be the order
* of a couple of megabytes.
*
* Since: 3.28
*/
struct _IdeCompileCommands
{
GObject parent_instance;
GHashTable *info_by_file;
guint has_loaded : 1;
};
typedef struct
{
GFile *directory;
GFile *file;
gchar *command;
} CompileInfo;
G_DEFINE_TYPE (IdeCompileCommands, ide_compile_commands, G_TYPE_OBJECT)
static void
compile_info_free (gpointer data)
{
CompileInfo *info = data;
if (info != NULL)
{
g_clear_object (&info->directory);
g_clear_object (&info->file);
g_clear_pointer (&info->command, g_free);
g_slice_free (CompileInfo, info);
}
}
static void
ide_compile_commands_finalize (GObject *object)
{
IdeCompileCommands *self = (IdeCompileCommands *)object;
g_clear_pointer (&self->info_by_file, g_hash_table_unref);
G_OBJECT_CLASS (ide_compile_commands_parent_class)->finalize (object);
}
static void
ide_compile_commands_class_init (IdeCompileCommandsClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = ide_compile_commands_finalize;
}
static void
ide_compile_commands_init (IdeCompileCommands *self)
{
}
/**
* ide_compile_commands_new:
*
* Creates a new #IdeCompileCommands object which can be used to parse
* clang-style compile commands database files (compile_commands.json).
*
* Returns: The newly created #IdeCompileCommands
*
* Since: 3.28
*/
IdeCompileCommands *
ide_compile_commands_new (void)
{
return g_object_new (IDE_TYPE_COMPILE_COMMANDS, NULL);
}
static void
ide_compile_commands_load_worker (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
IdeCompileCommands *self = source_object;
GFile *gfile = task_data;
g_autoptr(JsonParser) parser = NULL;
g_autoptr(GError) error = NULL;
g_autoptr(GHashTable) info_by_file = NULL;
g_autoptr(GHashTable) directories_by_path = NULL;
g_autofree gchar *contents = NULL;
JsonNode *root;
JsonArray *ar;
gsize len = 0;
guint n_items;
IDE_ENTRY;
g_assert (G_IS_TASK (task));
g_assert (IDE_IS_COMPILE_COMMANDS (self));
g_assert (G_IS_FILE (gfile));
g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
parser = json_parser_new ();
if (!g_file_load_contents (gfile, cancellable, &contents, &len, NULL, &error) ||
!json_parser_load_from_data (parser, contents, len, &error))
{
g_task_return_error (task, g_steal_pointer (&error));
IDE_EXIT;
}
if (NULL == (root = json_parser_get_root (parser)) ||
!JSON_NODE_HOLDS_ARRAY (root) ||
NULL == (ar = json_node_get_array (root)))
{
g_task_return_new_error (task,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Failed to extract commands, invalid json");
IDE_EXIT;
}
info_by_file = g_hash_table_new_full (g_file_hash,
(GEqualFunc)g_file_equal,
NULL,
compile_info_free);
directories_by_path = g_hash_table_new_full (g_str_hash,
g_str_equal,
NULL,
g_object_unref);
n_items = json_array_get_length (ar);
for (guint i = 0; i < n_items; i++)
{
CompileInfo *info;
JsonNode *item;
JsonNode *value;
JsonObject *obj;
GFile *dir;
const gchar *directory = NULL;
const gchar *file = NULL;
const gchar *command = NULL;
item = json_array_get_element (ar, i);
/* Skip past this node if its invalid for some reason, so we
* can try to be tolerante of errors created by broken tooling.
*/
if (item == NULL ||
!JSON_NODE_HOLDS_OBJECT (item) ||
NULL == (obj = json_node_get_object (item)))
continue;
if (json_object_has_member (obj, "file") &&
NULL != (value = json_object_get_member (obj, "file")) &&
JSON_NODE_HOLDS_VALUE (value))
file = json_node_get_string (value);
if (json_object_has_member (obj, "directory") &&
NULL != (value = json_object_get_member (obj, "directory")) &&
JSON_NODE_HOLDS_VALUE (value))
directory = json_node_get_string (value);
if (json_object_has_member (obj, "command") &&
NULL != (value = json_object_get_member (obj, "command")) &&
JSON_NODE_HOLDS_VALUE (value))
command = json_node_get_string (value);
/* Ignore items that are missing something or other */
if (file == NULL || command == NULL || directory == NULL)
continue;
/* Try to reduce the number of GFile we have for directories */
if (NULL == (dir = g_hash_table_lookup (directories_by_path, directory)))
{
dir = g_file_new_for_path (directory);
g_hash_table_insert (directories_by_path, (gchar *)directory, dir);
}
info = g_slice_new (CompileInfo);
info->file = g_file_resolve_relative_path (dir, file);
info->directory = g_object_ref (dir);
info->command = g_strdup (command);
g_hash_table_insert (info_by_file, info->file, info);
}
self->info_by_file = g_steal_pointer (&info_by_file);
g_task_return_boolean (task, TRUE);
IDE_EXIT;
}
/**
* ide_compile_commands_load:
* @self: An #IdeCompileCommands
* @file: A #GFile
* @cancellable: (nullable): A #GCancellable, or %NULL
* @error: A location for a #GError, or %NULL
*
* Synchronously loads the contents of the requested @file and parses
* the JSON command database contained within.
*
* You may only call this function once on an #IdeCompileCommands object.
* If there is a failure, you must create a new #IdeCompileCommands instance
* instead of calling this function again.
*
* See also: ide_compile_commands_load_async()
*
* Returns: %TRUE if successful; otherwise %FALSE and @error is set.
*
* Since: 3.28
*/
gboolean
ide_compile_commands_load (IdeCompileCommands *self,
GFile *file,
GCancellable *cancellable,
GError **error)
{
g_autoptr(GTask) task = NULL;
gboolean ret;
IDE_ENTRY;
g_return_val_if_fail (IDE_IS_COMPILE_COMMANDS (self), FALSE);
g_return_val_if_fail (self->has_loaded == FALSE, FALSE);
g_return_val_if_fail (G_IS_FILE (file), FALSE);
g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
self->has_loaded = TRUE;
task = g_task_new (self, cancellable, NULL, NULL);
g_task_set_priority (task, G_PRIORITY_LOW);
g_task_set_source_tag (task, ide_compile_commands_load);
g_task_set_task_data (task, g_object_ref (file), g_object_unref);
g_task_run_in_thread_sync (task, ide_compile_commands_load_worker);
ret = g_task_propagate_boolean (task, error);
IDE_RETURN (ret);
}
/**
* ide_compile_commands_load_async:
* @self: An #IdeCompileCommands
* @file: A #GFile
* @cancellable: (nullable): A #GCancellable, or %NULL
* @callback: the callback for the async operation
* @user_data: user data for @callback
*
* Asynchronously loads the contents of the requested @file and parses
* the JSON command database contained within.
*
* You may only call this function once on an #IdeCompileCommands object.
* If there is a failure, you must create a new #IdeCompileCommands instance
* instead of calling this function again.
*
* See also: ide_compile_commands_load_finish()
*
* Since: 3.28
*/
void
ide_compile_commands_load_async (IdeCompileCommands *self,
GFile *file,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autoptr(GTask) task = NULL;
IDE_ENTRY;
g_return_if_fail (IDE_IS_COMPILE_COMMANDS (self));
g_return_if_fail (self->has_loaded == FALSE);
g_return_if_fail (G_IS_FILE (file));
g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
self->has_loaded = TRUE;
task = g_task_new (self, cancellable, callback, user_data);
g_task_set_priority (task, G_PRIORITY_LOW);
g_task_set_source_tag (task, ide_compile_commands_load_async);
g_task_set_task_data (task, g_object_ref (file), g_object_unref);
g_task_run_in_thread (task, ide_compile_commands_load_worker);
IDE_EXIT;
}
/**
* ide_compile_commands_load_finish:
* @self: An #IdeCompileCommands
* @result: A #GAsyncResult provided to the callback
* @error: A location for a #GError, or %NULL
*
* Completes an asynchronous request to ide_compile_commands_load_async().
*
* See also: ide_compile_commands_load_async()
*
* Returns: %TRUE if the file was loaded successfully; otherwise %FALSE
* and @error is set.
*
* Since: 3.28
*/
gboolean
ide_compile_commands_load_finish (IdeCompileCommands *self,
GAsyncResult *result,
GError **error)
{
gboolean ret;
IDE_ENTRY;
g_return_val_if_fail (IDE_IS_COMPILE_COMMANDS (self), FALSE);
g_return_val_if_fail (G_IS_TASK (result), FALSE);
ret = g_task_propagate_boolean (G_TASK (result), error);
IDE_RETURN (ret);
}
/**
* ide_compile_commands_lookup:
* @self: An #IdeCompileCommands
* @file: A #GFile representing the file to lookup
* @directory: (out) (optional) (transfer full): A location for a #GFile, or %NULL
* @error: A location for a #GError, or %NULL
*
* Locates the commands to compile the @file requested.
*
* If @directory is non-NULL, then the directory to run the command from
* is placed in @directory.
*
* Returns: (nullable) (transfer full): A string array or %NULL if
* there was a failure to locate or parse the command.
*
* Since: 3.28
*/
gchar **
ide_compile_commands_lookup (IdeCompileCommands *self,
GFile *file,
GFile **directory,
GError **error)
{
CompileInfo *info;
g_auto(GStrv) argv = NULL;
gint argc = 0;
g_return_val_if_fail (IDE_IS_COMPILE_COMMANDS (self), NULL);
g_return_val_if_fail (G_IS_FILE (file), NULL);
if (self->info_by_file != NULL &&
NULL != (info = g_hash_table_lookup (self->info_by_file, file)))
{
if (!g_shell_parse_argv (info->command, &argc, &argv, error))
return NULL;
if (directory != NULL)
*directory = g_object_ref (info->directory);
return g_steal_pointer (&argv);
}
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_NOT_FOUND,
"Failed to locate command for requested file");
return NULL;
}
/* ide-compile-commands.h
*
* Copyright © 2017 Christian Hergert <chergert@redhat.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <gio/gio.h>
G_BEGIN_DECLS
#define IDE_TYPE_COMPILE_COMMANDS (ide_compile_commands_get_type())
G_DECLARE_FINAL_TYPE (IdeCompileCommands, ide_compile_commands, IDE, COMPILE_COMMANDS, GObject)
IdeCompileCommands *ide_compile_commands_new (void);
gboolean ide_compile_commands_load (IdeCompileCommands *self,
GFile *file,
GCancellable *cancellable,
GError **error);
void ide_compile_commands_load_async (IdeCompileCommands *self,
GFile *file,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data);
gboolean ide_compile_commands_load_finish (IdeCompileCommands *self,
GAsyncResult *result,
GError **error);
gchar **ide_compile_commands_lookup (IdeCompileCommands *self,
GFile *file,
GFile **directory,
GError **error);
G_END_DECLS
......@@ -11,6 +11,7 @@ buildsystem_headers = [
'ide-build-system.h',
'ide-build-target.h',
'ide-build-utils.h',
'ide-compile-commands.h',
'ide-configuration-manager.h',
'ide-configuration-provider.h',
'ide-configuration.h',
......@@ -30,6 +31,7 @@ buildsystem_sources = [
'ide-build-system.c',
'ide-build-target.c',
'ide-build-utils.c',
'ide-compile-commands.c',
'ide-configuration-manager.c',
'ide-configuration-provider.c',
'ide-configuration.c',
......
......@@ -57,6 +57,7 @@ G_BEGIN_DECLS
#include "buildsystem/ide-build-system.h"
#include "buildsystem/ide-build-system-discovery.h"
#include "buildsystem/ide-build-target.h"
#include "buildsystem/ide-compile-commands.h"
#include "buildsystem/ide-configuration-manager.h"
#include "buildsystem/ide-configuration.h"
#include "buildsystem/ide-configuration-provider.h"
......
[
{
"directory": "/build/gnome-builder/build",
"command": "cc -Isubprojects/libgd/libgd/gd@sha -Isubprojects/libgd/libgd -I../subprojects/libgd/libgd -Isubprojects/libgd -I../subprojects/libgd -I/opt/gnome/include/gtk-3.0 -I/opt/gnome/include/pango-1.0 -I/opt/gnome/include/glib-2.0 -I/opt/gnome/lib/glib-2.0/include -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/freetype2 -I/usr/include/libpng16 -I/opt/gnome/include/harfbuzz -I/opt/gnome/include/gdk-pixbuf-2.0 -I/opt/gnome/include/gio-unix-2.0/ -I/opt/gnome/include/atk-1.0 -I/opt/gnome/include/at-spi2-atk/2.0 -I/opt/gnome/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib64/dbus-1.0/include -I/home/christian/Projects/gnome-builder/build -fdiagnostics-color=always -pipe -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -Wextra -std=gnu11 -O0 -g -DHAVE_CONFIG_H -D_GNU_SOURCE -DIDE_COMPILATION -ggdb -O0 -fno-omit-frame-pointer -fPIC -pthread -DLIBGD_TAGGED_ENTRY=1 '-DG_LOG_DOMAIN=\"libgd\"' -DG_DISABLE_DEPRECATED -MMD -MQ 'subprojects/libgd/libgd/gd@sha/gd-types-catalog.c.o' -MF 'subprojects/libgd/libgd/gd@sha/gd-types-catalog.c.o.d' -o 'subprojects/libgd/libgd/gd@sha/gd-types-catalog.c.o' -c ../subprojects/libgd/libgd/gd-types-catalog.c",
"file": "../subprojects/libgd/libgd/gd-types-catalog.c"
},
{
"directory": "/build/gnome-builder/build",
"command": "cc -Isubprojects/libgd/libgd/gd@sha -Isubprojects/libgd/libgd -I../subprojects/libgd/libgd -Isubprojects/libgd -I../subprojects/libgd -I/opt/gnome/include/gtk-3.0 -I/opt/gnome/include/pango-1.0 -I/opt/gnome/include/glib-2.0 -I/opt/gnome/lib/glib-2.0/include -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/freetype2 -I/usr/include/libpng16 -I/opt/gnome/include/harfbuzz -I/opt/gnome/include/gdk-pixbuf-2.0 -I/opt/gnome/include/gio-unix-2.0/ -I/opt/gnome/include/atk-1.0 -I/opt/gnome/include/at-spi2-atk/2.0 -I/opt/gnome/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib64/dbus-1.0/include -I/home/christian/Projects/gnome-builder/build -fdiagnostics-color=always -pipe -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -Wextra -std=gnu11 -O0 -g -DHAVE_CONFIG_H -D_GNU_SOURCE -DIDE_COMPILATION -ggdb -O0 -fno-omit-frame-pointer -fPIC -pthread -DLIBGD_TAGGED_ENTRY=1 '-DG_LOG_DOMAIN=\"libgd\"' -DG_DISABLE_DEPRECATED -MMD -MQ 'subprojects/libgd/libgd/gd@sha/gd-tagged-entry.c.o' -MF 'subprojects/libgd/libgd/gd@sha/gd-tagged-entry.c.o.d' -o 'subprojects/libgd/libgd/gd@sha/gd-tagged-entry.c.o' -c ../subprojects/libgd/libgd/gd-tagged-entry.c",
"file": "../subprojects/libgd/libgd/gd-tagged-entry.c"
}
]
......@@ -20,6 +20,14 @@ ide_test_deps = [
gnome_builder_plugins_dep,
]
ide_compile_commands = executable('test-ide-compile-commands', 'test-ide-compile-commands.c',
c_args: ide_test_cflags,
dependencies: [ ide_test_deps ],
)
test('test-ide-compile-commands', ide_compile_commands, env: ide_test_env)
ide_context = executable('test-ide-context',
'test-ide-context.c',
c_args: ide_test_cflags,
......
/* test-ide-compile-commands.c
*
* Copyright © 2017 Christian Hergert <chergert@redhat.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <ide.h>
static void
test_compile_commands_basic (void)
{
g_autoptr(IdeCompileCommands) commands = NULL;
g_autoptr(GFile) missing = g_file_new_for_path ("missing");
g_autoptr(GFile) data_file = NULL;
g_autoptr(GFile) expected_file = NULL;
g_autoptr(GFile) dir = NULL;
g_autoptr(GError) error = NULL;
g_autofree gchar *data_path = NULL;
g_autofree gchar *dir_path = NULL;
g_auto(GStrv) cmdstrv = NULL;
gboolean r;
commands = ide_compile_commands_new ();
/* Test missing info before we've loaded */
g_assert (NULL == ide_compile_commands_lookup (commands, missing, NULL, NULL));
/* Now load our test file */
data_path = g_build_filename (TEST_DATA_DIR, "test-ide-compile-commands.json", NULL);
data_file = g_file_new_for_path (data_path);
r = ide_compile_commands_load (commands, data_file, NULL, &error);
g_assert_no_error (error);
g_assert_cmpint (r, ==, TRUE);
/* Now lookup a file that should exist in the database */
expected_file = g_file_new_for_path ("/build/gnome-builder/subprojects/libgd/libgd/gd-types-catalog.c");
cmdstrv = ide_compile_commands_lookup (commands, expected_file, &dir, &error);
g_assert_no_error (error);
g_assert (cmdstrv != NULL);
g_assert_cmpstr (cmdstrv[0], ==, "cc");
g_assert_cmpstr (cmdstrv[1], ==, "-Isubprojects/libgd/libgd/gd@sha");
dir_path = g_file_get_path (dir);
g_assert_cmpstr (dir_path, ==, "/build/gnome-builder/build");
}
gint
main (gint argc,
gchar *argv[])
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/Ide/CompileCommands/basic", test_compile_commands_basic);
return g_test_run ();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment