ide-ctags-builder.c 12.5 KB
Newer Older
1
2
/* ide-ctags-builder.c
 *
3
 * Copyright 2017-2019 Christian Hergert <chergert@redhat.com>
4
5
6
7
8
9
10
11
12
13
14
15
16
 *
 * 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/>.
17
18
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
19
20
21
22
 */

#define G_LOG_DOMAIN "ide-ctags-builder"

23
24
#include <libide-vcs.h>

25
26
27
28
#include "ide-ctags-builder.h"

struct _IdeCtagsBuilder
{
29
  IdeObject  parent;
30
31
};

32
33
34
35
36
37
38
typedef struct
{
  GFile *directory;
  GFile *destination;
  gchar *ctags;
  guint  recursive : 1;
} BuildTaskData;
39

40
static void tags_builder_iface_init (IdeTagsBuilderInterface *iface);
41

42
43
G_DEFINE_TYPE_WITH_CODE (IdeCtagsBuilder, ide_ctags_builder, IDE_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (IDE_TYPE_TAGS_BUILDER, tags_builder_iface_init))
44

45
static GHashTable *ignored;
46
47

static void
48
build_task_data_free (gpointer data)
49
{
50
  BuildTaskData *task_data = data;
51

52
53
  g_clear_object (&task_data->directory);
  g_clear_object (&task_data->destination);
54
  g_clear_pointer (&task_data->ctags, g_free);
55

56
57
  g_slice_free (BuildTaskData, task_data);
}
58

59
60
61
62
static void
ide_ctags_builder_class_init (IdeCtagsBuilderClass *klass)
{
  ignored = g_hash_table_new (g_str_hash, g_str_equal);
63

64
65
66
  /* TODO: We need a really fast, *THREAD-SAFE* access to determine
   *       if files are ignored via the VCS.
   */
67

68
69
70
71
72
73
74
75
  g_hash_table_insert (ignored, (gchar *)".git", NULL);
  g_hash_table_insert (ignored, (gchar *)".bzr", NULL);
  g_hash_table_insert (ignored, (gchar *)".svn", NULL);
  g_hash_table_insert (ignored, (gchar *)".flatpak-builder", NULL);
  g_hash_table_insert (ignored, (gchar *)".libs", NULL);
  g_hash_table_insert (ignored, (gchar *)".deps", NULL);
  g_hash_table_insert (ignored, (gchar *)"autom4te.cache", NULL);
  g_hash_table_insert (ignored, (gchar *)"build-aux", NULL);
76
77
78
}

static void
79
ide_ctags_builder_init (IdeCtagsBuilder *self)
80
{
81
}
82

83
IdeTagsBuilder *
84
ide_ctags_builder_new (void)
85
{
86
  return g_object_new (IDE_TYPE_CTAGS_BUILDER, NULL);
87
88
}

89
90
91
92
93
94
95
static gboolean
ide_ctags_builder_build (IdeCtagsBuilder *self,
                         const gchar     *ctags,
                         GFile           *directory,
                         GFile           *destination,
                         gboolean         recursive,
                         GCancellable    *cancellable)
96
{
97
  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
98
99
100
101
102
103
104
105
  g_autoptr(IdeSubprocess) subprocess = NULL;
  g_autoptr(GPtrArray) directories = NULL;
  g_autoptr(GPtrArray) dest_directories = NULL;
  g_autoptr(GFile) tags_file = NULL;
  g_autoptr(GFileEnumerator) enumerator = NULL;
  g_autoptr(GError) error = NULL;
  g_autofree gchar *cwd = NULL;
  g_autofree gchar *dest_dir = NULL;
106
  g_autofree gchar *options_path = NULL;
107
108
  g_autofree gchar *tags_path = NULL;
  g_autoptr(GString) filenames = NULL;
109
  g_autoptr(IdeVcs) vcs = NULL;
110
  GOutputStream *stdin_stream;
111
  IdeContext *context;
112
  gpointer infoptr;
Christian Hergert's avatar
Christian Hergert committed
113

114
  g_assert (IDE_IS_CTAGS_BUILDER (self));
115
116
  g_assert (G_IS_FILE (directory));
  g_assert (G_IS_FILE (destination));
117

118
119
120
  if (g_cancellable_is_cancelled (cancellable))
    return FALSE;

121
  context = ide_object_get_context (IDE_OBJECT (self));
122
  vcs = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_VCS);
123

124
125
126
127
128
129
130
  dest_dir = g_file_get_path (destination);
  if (0 != g_mkdir_with_parents (dest_dir, 0750))
    return FALSE;

  tags_file = g_file_get_child (destination, "tags");
  tags_path = g_file_get_path (tags_file);
  cwd = g_file_get_path (directory);
131
132
133
134
  options_path = g_build_filename (g_get_user_config_dir (),
                                   ide_get_program_name (),
                                   "ctags.conf",
                                   NULL);
135
136
137
  directories = g_ptr_array_new_with_free_func (g_object_unref);
  dest_directories = g_ptr_array_new_with_free_func (g_object_unref);
  filenames = g_string_new (NULL);
138

139
140
  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDIN_PIPE |
                                          G_SUBPROCESS_FLAGS_STDERR_SILENCE);
141

142
143
144
  ide_subprocess_launcher_set_cwd (launcher, cwd);
  ide_subprocess_launcher_setenv (launcher, "TMPDIR", cwd, TRUE);
  ide_subprocess_launcher_set_stdout_file_path (launcher, tags_path);
145

146
147
148
149
#ifdef __linux__
  ide_subprocess_launcher_push_argv (launcher, "nice");
#endif

150
  ide_subprocess_launcher_push_argv (launcher, ctags);
151
152
153
154
155
156
  ide_subprocess_launcher_push_argv (launcher, "-f");
  ide_subprocess_launcher_push_argv (launcher, "-");
  ide_subprocess_launcher_push_argv (launcher, "--tag-relative=no");
  ide_subprocess_launcher_push_argv (launcher, "--exclude=.git");
  ide_subprocess_launcher_push_argv (launcher, "--exclude=.bzr");
  ide_subprocess_launcher_push_argv (launcher, "--exclude=.svn");
157
  ide_subprocess_launcher_push_argv (launcher, "--exclude=.flatpak-builder");
158
159
160
161
  ide_subprocess_launcher_push_argv (launcher, "--sort=yes");
  ide_subprocess_launcher_push_argv (launcher, "--languages=all");
  ide_subprocess_launcher_push_argv (launcher, "--file-scope=yes");
  ide_subprocess_launcher_push_argv (launcher, "--c-kinds=+defgpstx");
162

163
  if (g_file_test (options_path, G_FILE_TEST_IS_REGULAR))
164
165
166
167
    {
      ide_subprocess_launcher_push_argv (launcher, "--options");
      ide_subprocess_launcher_push_argv (launcher, options_path);
    }
168
169
170
171
172
173
174
175
176
177
178
179
180
181

  /* Read filenames from stdin, which we will provided below */
  ide_subprocess_launcher_push_argv (launcher, "-L");
  ide_subprocess_launcher_push_argv (launcher, "-");

  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error);

  if (subprocess == NULL)
    {
      g_warning ("%s", error->message);
      return FALSE;
    }

  stdin_stream = ide_subprocess_get_stdin_pipe (subprocess);
182
183

  /*
184
185
186
187
188
189
190
   * We do our own recursive building of ctags instead of --recursive=yes
   * so that we can have smaller files to update. This helps on larger
   * projects where we would have to rescan the whole project after a
   * file is saved.
   *
   * Additionally, while walking the file-system tree, we append files
   * to stdin of our ctags process to tell it to process them.
191
   */
192

193
  enumerator = g_file_enumerate_children (directory,
194
                                          G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK","
195
196
197
198
199
                                          G_FILE_ATTRIBUTE_STANDARD_NAME","
                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
                                          G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                          cancellable,
                                          &error);
200

201
202
203
  if (enumerator == NULL)
    IDE_GOTO (finish_subprocess);

204
  while ((infoptr = g_file_enumerator_next_file (enumerator, cancellable, &error)))
205
    {
206
207
208
209
210
211
212
213
214
215
      g_autoptr(GFileInfo) info = infoptr;
      const gchar *name;
      GFileType type;

      name = g_file_info_get_name (info);
      type = g_file_info_get_file_type (info);

      if (g_hash_table_contains (ignored, name))
        continue;

216
217
218
      if (g_file_info_get_is_symlink (info))
        continue;

219
220
221
222
223
224
225
226
227
228
229
230
      if (type == G_FILE_TYPE_DIRECTORY)
        {
          if (recursive)
            {
              g_ptr_array_add (directories, g_file_get_child (directory, name));
              g_ptr_array_add (dest_directories, g_file_get_child (destination, name));
            }
        }
      else if (type == G_FILE_TYPE_REGULAR)
        {
          g_string_append_printf (filenames, "%s\n", name);
        }
231
232
    }

233
  g_output_stream_write_all (stdin_stream, filenames->str, filenames->len, NULL, NULL, NULL);
234

235
236
finish_subprocess:
  g_output_stream_close (stdin_stream, NULL, NULL);
Christian Hergert's avatar
Christian Hergert committed
237

238
239
240
241
242
  if (!ide_subprocess_wait_check (subprocess, NULL, &error))
    {
      g_warning ("%s", error->message);
      return FALSE;
    }
243

244
245
246
247
  for (guint i = 0; i < directories->len; i++)
    {
      GFile *child = g_ptr_array_index (directories, i);
      GFile *dest_child = g_ptr_array_index (dest_directories, i);
248

249
250
      g_assert (G_IS_FILE (child));
      g_assert (G_IS_FILE (dest_child));
251

252
253
254
255
      if (ide_object_in_destruction (IDE_OBJECT (self)) ||
          g_cancellable_is_cancelled (cancellable))
        return FALSE;

256
257
258
      if (ide_vcs_is_ignored (vcs, child, NULL))
        continue;

259
260
261
      if (!ide_ctags_builder_build (self, ctags, child, dest_child, recursive, cancellable))
        return FALSE;
    }
262

263
  return TRUE;
264
265
}

266
static void
267
ide_ctags_builder_build_worker (IdeTask      *task,
268
269
270
                                gpointer      source_object,
                                gpointer      task_data_ptr,
                                GCancellable *cancellable)
271
{
272
273
274
  BuildTaskData *task_data = task_data_ptr;
  IdeCtagsBuilder *self = source_object;
  const gchar *ctags;
275
  g_autofree gchar *program = NULL;
276

277
  IDE_ENTRY;
278

279
  g_assert (IDE_IS_TASK (task));
280
281
  g_assert (IDE_IS_CTAGS_BUILDER (source_object));
  g_assert (G_IS_FILE (task_data->directory));
282

283
  ctags = task_data->ctags;
284
285
  program = g_find_program_in_path (ctags);
  if (!program)
286
    ctags = "ctags";
287

288
289
290
291
292
293
  ide_ctags_builder_build (self,
                           ctags,
                           task_data->directory,
                           task_data->destination,
                           task_data->recursive,
                           cancellable);
294

295
  ide_task_return_boolean (task, TRUE);
296

297
  IDE_EXIT;
298
299
300
}

static void
301
302
303
304
305
306
ide_ctags_builder_build_async (IdeTagsBuilder      *builder,
                               GFile               *directory_or_file,
                               gboolean             recursive,
                               GCancellable        *cancellable,
                               GAsyncReadyCallback  callback,
                               gpointer             user_data)
307
{
308
  IdeCtagsBuilder *self = (IdeCtagsBuilder *)builder;
309
  g_autoptr(IdeTask) task = NULL;
310
311
312
  g_autoptr(GSettings) settings = NULL;
  g_autofree gchar *destination_path = NULL;
  g_autofree gchar *relative_path = NULL;
313
  g_autoptr(GFile) workdir = NULL;
314
315
  BuildTaskData *task_data;
  IdeContext *context;
316

317
318
319
320
321
  IDE_ENTRY;

  g_assert (IDE_IS_CTAGS_BUILDER (self));
  g_assert (G_IS_FILE (directory_or_file));

322
323
324
325
  task = ide_task_new (self, cancellable, callback, user_data);
  ide_task_set_source_tag (task, ide_ctags_builder_build_async);
  ide_task_set_priority (task, G_PRIORITY_LOW + 200);

326
327
  if (ide_object_in_destruction (IDE_OBJECT (self)) ||
      !(context = ide_object_get_context (IDE_OBJECT (self))))
328
329
330
331
332
333
334
335
    {
      ide_task_return_new_error (task,
                                 G_IO_ERROR,
                                 G_IO_ERROR_CANCELLED,
                                 "The operation was cancelled");
      IDE_EXIT;
    }

336
337
338
339
340
341
342
343
  settings = g_settings_new ("org.gnome.builder.code-insight");

  task_data = g_slice_new0 (BuildTaskData);
  task_data->ctags = g_settings_get_string (settings, "ctags-path");
  task_data->directory = g_object_ref (directory_or_file);
  task_data->recursive = recursive;

  /*
344
345
346
347
   * The destination directory for the tags should match the hierarchy of the
   * projects source tree, but be based in something like
   * ~/.cache/gnome-builder/projects/$project_id/ctags/ so that they can be
   * reused even between configuration changes. Primarily, we want to avoid
348
349
   * putting things in the source tree.
   */
350
  workdir = ide_context_ref_workdir (context);
351
  relative_path = g_file_get_relative_path (workdir, directory_or_file);
352
  destination_path = ide_context_cache_filename (context, "ctags", relative_path, NULL);
353
354
  task_data->destination = g_file_new_for_path (destination_path);

355
356
357
  ide_task_set_task_data (task, task_data, build_task_data_free);
  ide_task_set_kind (task, IDE_TASK_KIND_INDEXER);
  ide_task_run_in_thread (task, ide_ctags_builder_build_worker);
358
359

  IDE_EXIT;
360
361
}

362
363
364
365
static gboolean
ide_ctags_builder_build_finish (IdeTagsBuilder  *builder,
                                GAsyncResult    *result,
                                GError         **error)
366
{
367
  gboolean ret;
368

369
  IDE_ENTRY;
370

371
  g_return_val_if_fail (IDE_IS_CTAGS_BUILDER (builder), FALSE);
372
  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
373

374
  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
375

376
  IDE_RETURN (ret);
377
}
378

379
380
static void
tags_builder_iface_init (IdeTagsBuilderInterface *iface)
381
{
382
383
  iface->build_async = ide_ctags_builder_build_async;
  iface->build_finish = ide_ctags_builder_build_finish;
384
}