diff options
author | Philip Chimento <philip.chimento@gmail.com> | 2021-11-07 00:28:47 +0000 |
---|---|---|
committer | Philip Chimento <philip.chimento@gmail.com> | 2021-11-07 00:28:47 +0000 |
commit | 4fa8279ebff4ab462f26370b68599d6f401df8d2 (patch) | |
tree | d355adfe10fb01742c6276ec08c1bc32b1be4450 | |
parent | 69ff99f62255b7c10bfdbe65f61c51790655a6f1 (diff) | |
parent | 42f8ebcb3e8a8af536816fbaac28a3470bad06ea (diff) | |
download | gjs-4fa8279ebff4ab462f26370b68599d6f401df8d2.tar.gz |
Merge branch 'ewlsh/implicit-mainloop' into 'master'
[Mainloop 1/3] Add custom GSource for promise queueing
Closes #1
See merge request GNOME/gjs!557
-rw-r--r-- | doc/Custom-GSources.md | 22 | ||||
-rw-r--r-- | gjs/context-private.h | 9 | ||||
-rw-r--r-- | gjs/context.cpp | 56 | ||||
-rw-r--r-- | gjs/promise.cpp | 177 | ||||
-rw-r--r-- | gjs/promise.h | 54 | ||||
-rw-r--r-- | installed-tests/js/.eslintrc.yml | 1 | ||||
-rw-r--r-- | installed-tests/js/meson.build | 1 | ||||
-rw-r--r-- | installed-tests/js/testAsync.js | 67 | ||||
-rw-r--r-- | meson.build | 1 | ||||
-rwxr-xr-x | tools/process_iwyu.py | 2 | ||||
-rwxr-xr-x | tools/run_iwyu.sh | 2 |
11 files changed, 351 insertions, 41 deletions
diff --git a/doc/Custom-GSources.md b/doc/Custom-GSources.md new file mode 100644 index 00000000..de625d79 --- /dev/null +++ b/doc/Custom-GSources.md @@ -0,0 +1,22 @@ +## Custom GSources + +GLib allows custom GSources to be added to the main loop. +A custom GSource can control under what conditions it is dispatched. +You can read more about GLib's main loop [here][glib-mainloop-docs]. + +Within GJS, we have implemented a custom GSource to handle Promise execution. +It dispatches whenever a Promise is queued, occurring before any other GLib +events. +This mimics the behavior of a [microtask queue](mdn-microtasks) in other +JavaScript environments. +You can read an introduction to building custom GSources within the archived +developer documentation [here][custom-gsource-tutorial] or, if unavailable, via +[the original source code][custom-gsource-tutorial-source]. +Another great resource is Philip Withnall's ["A detailed look at GSource"][gsource-blog-post]<sup>[[permalink]][gsource-blog-post-archive]</sup>. + +[gsource-blog-post]: https://tecnocode.co.uk/2015/05/05/a-detailed-look-at-gsource/ +[gsource-blog-post-archive]: https://web.archive.org/web/20201013000618/https://tecnocode.co.uk/2015/05/05/a-detailed-look-at-gsource/ +[mdn-microtasks]: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide +[glib-mainloop-docs]: https://docs.gtk.org/glib/main-loop.html#creating-new-source-types +[custom-gsource-tutorial]: https://developer-old.gnome.org/gnome-devel-demos/unstable/custom-gsource.c.html.en +[custom-gsource-tutorial-source]: https://gitlab.gnome.org/Archive/gnome-devel-docs/-/blob/703816cec292293fd337b6db8520b9b0afa7b3c9/platform-demos/C/custom-gsource.c.page diff --git a/gjs/context-private.h b/gjs/context-private.h index 75b04bf0..0499d286 100644 --- a/gjs/context-private.h +++ b/gjs/context-private.h @@ -17,6 +17,7 @@ #include <utility> // for pair #include <vector> +#include <gio/gio.h> #include <glib-object.h> #include <glib.h> @@ -37,6 +38,7 @@ #include "gjs/jsapi-util.h" #include "gjs/macros.h" #include "gjs/profiler.h" +#include "gjs/promise.h" namespace js { class SystemAllocPolicy; @@ -78,7 +80,7 @@ class GjsContextPrivate : public JS::JobQueue { std::vector<std::string> m_args; JobQueueStorage m_job_queue; - unsigned m_idle_drain_handler; + Gjs::PromiseJobDispatcher m_dispatcher; std::vector<std::pair<DestroyNotify, void*>> m_destroy_notifications; std::vector<Gjs::Closure::Ptr> m_async_closures; @@ -134,7 +136,6 @@ class GjsContextPrivate : public JS::JobQueue { class SavedQueue; void start_draining_job_queue(void); void stop_draining_job_queue(void); - static gboolean drain_job_queue_idle_handler(void* data); uint8_t handle_exit_code(const char* type, const char* identifier, GError** error); @@ -236,11 +237,13 @@ class GjsContextPrivate : public JS::JobQueue { JS::HandleObject allocation_site, JS::HandleObject incumbent_global) override; void runJobs(JSContext* cx) override; + void runJobs(JSContext* cx, GCancellable* cancellable); [[nodiscard]] bool empty() const override { return m_job_queue.empty(); } js::UniquePtr<JS::JobQueue::SavedJobQueue> saveJobQueue( JSContext* cx) override; - GJS_JSAPI_RETURN_CONVENTION bool run_jobs_fallible(void); + GJS_JSAPI_RETURN_CONVENTION bool run_jobs_fallible( + GCancellable* cancellable = nullptr); void register_unhandled_promise_rejection(uint64_t id, GjsAutoChar&& stack); void unregister_unhandled_promise_rejection(uint64_t id); void warn_about_unhandled_promise_rejections(); diff --git a/gjs/context.cpp b/gjs/context.cpp index 734ac68d..9a75d596 100644 --- a/gjs/context.cpp +++ b/gjs/context.cpp @@ -81,6 +81,7 @@ #include "gjs/objectbox.h" #include "gjs/profiler-private.h" #include "gjs/profiler.h" +#include "gjs/promise.h" #include "gjs/text-encoding.h" #include "modules/modules.h" #include "util/log.h" @@ -405,6 +406,8 @@ void GjsContextPrivate::unregister_notifier(DestroyNotify notify_func, void GjsContextPrivate::dispose(void) { if (m_cx) { + stop_draining_job_queue(); + gjs_debug(GJS_DEBUG_CONTEXT, "Notifying reference holders of GjsContext dispose"); @@ -532,8 +535,8 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context) : m_public_context(public_context), m_cx(cx), m_owner_thread(std::this_thread::get_id()), + m_dispatcher(this), m_environment_preparer(cx) { - JS_SetGCCallback( cx, [](JSContext*, JSGCStatus status, JS::GCReason reason, void* data) { @@ -654,6 +657,8 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context) cx, "resource:///org/gnome/gjs/modules/esm/_bootstrap/default.js", "ESM bootstrap"); } + + start_draining_job_queue(); } void GjsContextPrivate::set_args(std::vector<std::string>&& args) { @@ -895,32 +900,15 @@ bool GjsContextPrivate::should_exit(uint8_t* exit_code_p) const { } void GjsContextPrivate::start_draining_job_queue(void) { - if (!m_idle_drain_handler) { - gjs_debug(GJS_DEBUG_CONTEXT, "Starting promise job queue handler"); - m_idle_drain_handler = g_idle_add_full( - G_PRIORITY_DEFAULT, drain_job_queue_idle_handler, this, nullptr); - } + gjs_debug(GJS_DEBUG_CONTEXT, "Starting promise job dispatcher"); + m_dispatcher.start(); } void GjsContextPrivate::stop_draining_job_queue(void) { m_draining_job_queue = false; - if (m_idle_drain_handler) { - gjs_debug(GJS_DEBUG_CONTEXT, "Stopping promise job queue handler"); - g_source_remove(m_idle_drain_handler); - m_idle_drain_handler = 0; - } -} -gboolean GjsContextPrivate::drain_job_queue_idle_handler(void* data) { - gjs_debug(GJS_DEBUG_CONTEXT, "Promise job queue handler"); - auto* gjs = static_cast<GjsContextPrivate*>(data); - gjs->runJobs(gjs->context()); - /* Uncatchable exceptions are swallowed here - no way to get a handle on - * the main loop to exit it from this idle handler */ - gjs_debug(GJS_DEBUG_CONTEXT, "Promise job queue handler finished"); - g_assert(gjs->empty() && gjs->m_idle_drain_handler == 0 && - "GjsContextPrivate::runJobs() should have emptied queue"); - return G_SOURCE_REMOVE; + gjs_debug(GJS_DEBUG_CONTEXT, "Stopping promise job dispatcher"); + m_dispatcher.stop(); } JSObject* GjsContextPrivate::getIncumbentGlobal(JSContext* cx) { @@ -943,27 +931,24 @@ bool GjsContextPrivate::enqueuePromiseJob(JSContext* cx [[maybe_unused]], gjs_debug_object(job).c_str(), gjs_debug_object(promise).c_str(), gjs_debug_object(allocation_site).c_str()); - if (m_idle_drain_handler) - g_assert(!empty()); - else - g_assert(empty()); - if (!m_job_queue.append(job)) { JS_ReportOutOfMemory(m_cx); return false; } JS::JobQueueMayNotBeEmpty(m_cx); - start_draining_job_queue(); + m_dispatcher.start(); return true; } // Override of JobQueue::runJobs(). Called by js::RunJobs(), and when execution // of the job queue was interrupted by the debugger and is resuming. -void GjsContextPrivate::runJobs(JSContext* cx) { +void GjsContextPrivate::runJobs(JSContext* cx) { runJobs(cx, nullptr); } + +void GjsContextPrivate::runJobs(JSContext* cx, GCancellable* cancellable) { g_assert(cx == m_cx); g_assert(from_cx(cx) == this); - if (!run_jobs_fallible()) + if (!run_jobs_fallible(cancellable)) gjs_log_exception(cx); } @@ -979,7 +964,7 @@ void GjsContextPrivate::runJobs(JSContext* cx) { * Returns: false if one of the jobs threw an uncatchable exception; * otherwise true. */ -bool GjsContextPrivate::run_jobs_fallible(void) { +bool GjsContextPrivate::run_jobs_fallible(GCancellable* cancellable) { bool retval = true; if (m_draining_job_queue || m_should_exit) @@ -996,7 +981,7 @@ bool GjsContextPrivate::run_jobs_fallible(void) { * it's crucial to recheck the queue length during each iteration. */ for (size_t ix = 0; ix < m_job_queue.length(); ix++) { /* A previous job might have set this flag. e.g., System.exit(). */ - if (m_should_exit) + if (m_should_exit || g_cancellable_is_cancelled(cancellable)) break; job = m_job_queue[ix]; @@ -1032,8 +1017,8 @@ bool GjsContextPrivate::run_jobs_fallible(void) { } } + m_draining_job_queue = false; m_job_queue.clear(); - stop_draining_job_queue(); JS::JobQueueIsEmpty(m_cx); return retval; } @@ -1042,14 +1027,12 @@ class GjsContextPrivate::SavedQueue : public JS::JobQueue::SavedJobQueue { private: GjsContextPrivate* m_gjs; JS::PersistentRooted<JobQueueStorage> m_queue; - bool m_idle_was_pending : 1; bool m_was_draining : 1; public: explicit SavedQueue(GjsContextPrivate* gjs) : m_gjs(gjs), m_queue(gjs->m_cx, std::move(gjs->m_job_queue)), - m_idle_was_pending(gjs->m_idle_drain_handler != 0), m_was_draining(gjs->m_draining_job_queue) { gjs_debug(GJS_DEBUG_CONTEXT, "Pausing job queue"); gjs->stop_draining_job_queue(); @@ -1059,8 +1042,7 @@ class GjsContextPrivate::SavedQueue : public JS::JobQueue::SavedJobQueue { gjs_debug(GJS_DEBUG_CONTEXT, "Unpausing job queue"); m_gjs->m_job_queue = std::move(m_queue.get()); m_gjs->m_draining_job_queue = m_was_draining; - if (m_idle_was_pending) - m_gjs->start_draining_job_queue(); + m_gjs->start_draining_job_queue(); } }; diff --git a/gjs/promise.cpp b/gjs/promise.cpp new file mode 100644 index 00000000..ce19a780 --- /dev/null +++ b/gjs/promise.cpp @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later +// SPDX-FileCopyrightText: 2021 Evan Welsh <contact@evanwelsh.com> +// SPDX-FileCopyrightText: 2021 Marco Trevisan <mail@3v1n0.net> + +#include <config.h> + +#include <stddef.h> // for size_t + +#include <gio/gio.h> +#include <glib-object.h> + +#include "gjs/context-private.h" +#include "gjs/jsapi-util.h" +#include "gjs/promise.h" + +/** + * promise.cpp - This file implements a custom GSource, PromiseJobQueueSource, + * which handles promise dispatching within GJS. Custom GSources are able to + * control under which conditions they dispatch. PromiseJobQueueSource will + * always dispatch if even a single Promise is enqueued and will continue + * dispatching until all Promises (also known as "Jobs" within SpiderMonkey) + * are run. While this does technically mean Promises can starve the mainloop + * if run recursively, this is intentional. Within JavaScript Promises are + * considered "microtasks" and a microtask must run before any other task + * continues. + * + * PromiseJobQueueSource is attached to the thread's default GMainContext with + * a default priority of -1000. This is 10x the priority of G_PRIORITY_HIGH and + * no application code should attempt to override this. + * + * See doc/Custom-GSources.md for more background information on custom + * GSources and microtasks + */ + +namespace Gjs { + +/** + * @brief a custom GSource which handles draining our job queue. + */ +class PromiseJobDispatcher::Source : public GSource { + // The private GJS context this source runs within. + GjsContextPrivate* m_gjs; + // The main context this source attaches to. + GjsAutoMainContext m_main_context; + // The cancellable that stops this source. + GjsAutoUnref<GCancellable> m_cancellable; + GjsAutoPointer<GSource, GSource, g_source_unref> m_cancellable_source; + + // G_PRIORITY_HIGH is normally -100, we set 10 times that to ensure our + // source always has the greatest priority. This means our prepare will + // be called before other sources, and prepare will determine whether + // we dispatch. + static constexpr int PRIORITY = 10 * G_PRIORITY_HIGH; + + // GSource custom functions + static GSourceFuncs source_funcs; + + // Called to determine whether the source should run (dispatch) in the + // next event loop iteration. If the job queue is not empty we return true + // to schedule a dispatch. + gboolean prepare(int* timeout [[maybe_unused]]) { return !m_gjs->empty(); } + + gboolean dispatch() { + if (g_cancellable_is_cancelled(m_cancellable)) + return G_SOURCE_REMOVE; + + // The ready time is sometimes set to 0 to kick us out of polling, + // we need to reset the value here or this source will always be the + // next one to execute. (it will starve the other sources) + g_source_set_ready_time(this, -1); + + // A reference to the current cancellable is needed in case any + // jobs reset PromiseJobDispatcher and thus replace the cancellable. + GjsAutoUnref<GCancellable> cancellable(m_cancellable, + GjsAutoTakeOwnership{}); + // Drain the job queue. + m_gjs->runJobs(m_gjs->context(), cancellable); + + return G_SOURCE_CONTINUE; + } + + public: + /** + * @brief Constructs a new GjsPromiseJobQueueSource GSource and adds a + * reference to the associated main context. + * + * @param cx the current JSContext + * @param cancellable an optional cancellable + */ + Source(GjsContextPrivate* gjs, GMainContext* main_context) + : m_gjs(gjs), + m_main_context(main_context, GjsAutoTakeOwnership()), + m_cancellable(g_cancellable_new()), + m_cancellable_source(g_cancellable_source_new(m_cancellable)) { + g_source_set_priority(this, PRIORITY); +#if GLIB_CHECK_VERSION(2, 70, 0) + g_source_set_static_name(this, "GjsPromiseJobQueueSource"); +#else + g_source_set_name(this, "GjsPromiseJobQueueSource"); +#endif + + // Add our cancellable source to our main source, + // this will trigger the main source if our cancellable + // is cancelled. + g_source_add_child_source(this, m_cancellable_source); + } + + void* operator new(size_t size) { + return g_source_new(&source_funcs, size); + } + void operator delete(void* p) { g_source_unref(static_cast<GSource*>(p)); } + + bool is_running() { return !!g_source_get_context(this); } + + /** + * @brief Trigger the cancellable, detaching our source. + */ + void cancel() { g_cancellable_cancel(m_cancellable); } + /** + * @brief Reset the cancellable and prevent the source from stopping, + * overriding a previous cancel() call. Called by start() in + * PromiseJobDispatcher to ensure the custom source will start. + */ + void reset() { + if (!g_cancellable_is_cancelled(m_cancellable)) + return; + + if (is_running()) + g_source_remove_child_source(this, m_cancellable_source); + else + g_source_destroy(m_cancellable_source); + + // Drop the old cancellable and create a new one, as per + // https://docs.gtk.org/gio/method.Cancellable.reset.html + m_cancellable = g_cancellable_new(); + m_cancellable_source = g_cancellable_source_new(m_cancellable); + g_source_add_child_source(this, m_cancellable_source); + } +}; + +GSourceFuncs PromiseJobDispatcher::Source::source_funcs = { + [](GSource* source, int* timeout) { + return static_cast<Source*>(source)->prepare(timeout); + }, + nullptr, // check + [](GSource* source, GSourceFunc, void*) { + return static_cast<Source*>(source)->dispatch(); + }, + [](GSource* source) { static_cast<Source*>(source)->~Source(); }, +}; + +PromiseJobDispatcher::PromiseJobDispatcher(GjsContextPrivate* gjs) + // Acquire a guaranteed reference to this thread's default main context + : m_main_context(g_main_context_ref_thread_default()), + // Create and reference our custom GSource + m_source(std::make_unique<Source>(gjs, m_main_context)) {} + +PromiseJobDispatcher::~PromiseJobDispatcher() { + g_source_destroy(m_source.get()); +} + +bool PromiseJobDispatcher::is_running() { return m_source->is_running(); } + +void PromiseJobDispatcher::start() { + // Reset the cancellable + m_source->reset(); + + // Don't re-attach if the task is already running + if (is_running()) + return; + + g_source_attach(m_source.get(), m_main_context); +} + +void PromiseJobDispatcher::stop() { m_source->cancel(); } + +}; // namespace Gjs diff --git a/gjs/promise.h b/gjs/promise.h new file mode 100644 index 00000000..8fec2aeb --- /dev/null +++ b/gjs/promise.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later +// SPDX-FileCopyrightText: 2021 Evan Welsh <contact@evanwelsh.com> + +#pragma once + +#include <config.h> + +#include <memory> + +#include <glib.h> + +#include "gjs/jsapi-util.h" + +class GjsContextPrivate; + +using GjsAutoMainContext = + GjsAutoPointer<GMainContext, GMainContext, g_main_context_unref, + g_main_context_ref>; + +namespace Gjs { + +/** + * @brief A class which wraps a custom GSource and handles associating it with a + * GMainContext. While it is running, it will attach the source to the main + * context so that promise jobs are run at the appropriate time. + */ +class PromiseJobDispatcher { + class Source; + // The thread-default GMainContext + GjsAutoMainContext m_main_context; + // The custom source. + std::unique_ptr<Source> m_source; + + public: + explicit PromiseJobDispatcher(GjsContextPrivate*); + ~PromiseJobDispatcher(); + + /** + * @brief Start (or resume) dispatching jobs from the promise job queue + */ + void start(); + + /** + * @brief Stop dispatching + */ + void stop(); + + /** + * @brief Whether the dispatcher is currently running + */ + bool is_running(); +}; + +}; // namespace Gjs diff --git a/installed-tests/js/.eslintrc.yml b/installed-tests/js/.eslintrc.yml index abc9c527..bbf09ab2 100644 --- a/installed-tests/js/.eslintrc.yml +++ b/installed-tests/js/.eslintrc.yml @@ -32,6 +32,7 @@ globals: overrides: - files: - matchers.js + - testAsync.js - testCairoModule.js - testConsole.js - testESModules.js diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build index 5ca37103..2f007351 100644 --- a/installed-tests/js/meson.build +++ b/installed-tests/js/meson.build @@ -231,6 +231,7 @@ endif # minijasmine flag modules_tests = [ + 'Async', 'Console', 'ESModules', 'Encoding', diff --git a/installed-tests/js/testAsync.js b/installed-tests/js/testAsync.js new file mode 100644 index 00000000..dbd6979c --- /dev/null +++ b/installed-tests/js/testAsync.js @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later +// SPDX-FileCopyrightText: 2021 Evan Welsh <contact@evanwelsh.com> + +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; + +const PRIORITIES = [ + 'PRIORITY_LOW', + 'PRIORITY_HIGH', + 'PRIORITY_DEFAULT', + 'PRIORITY_HIGH_IDLE', + 'PRIORITY_DEFAULT_IDLE', +]; + +describe('Async microtasks resolves before', function () { + // Generate test suites with different types of Sources + const tests = [ + { + description: 'idle task with', + createSource: () => GLib.idle_source_new(), + }, + { + description: '0-second timeout task with', + // A timeout of 0 tests if microtasks (promises) run before + // non-idle tasks which would normally execute "next" in the loop + createSource: () => GLib.timeout_source_new(0), + }, + ]; + + for (const {description, createSource} of tests) { + describe(description, function () { + const CORRECT_TASKS = [ + 'async 1', + 'async 2', + 'source task', + ]; + + for (const priority of PRIORITIES) { + it(`priority set to GLib.${priority}`, function (done) { + const tasks = []; + + const source = createSource(); + source.set_priority(GLib[priority]); + GObject.source_set_closure(source, () => { + tasks.push('source task'); + + expect(tasks).toEqual(jasmine.arrayWithExactContents(CORRECT_TASKS)); + + done(); + source.destroy(); + + return GLib.SOURCE_REMOVE; + }); + source.attach(null); + + (async () => { + // Without an await async functions execute + // synchronously + tasks.push(await 'async 1'); + })().then(() => { + tasks.push('async 2'); + }); + }); + } + }); + } +}); diff --git a/meson.build b/meson.build index 78bfc8a6..437b3fd3 100644 --- a/meson.build +++ b/meson.build @@ -424,6 +424,7 @@ libgjs_sources = [ 'gjs/objectbox.cpp', 'gjs/objectbox.h', 'gjs/profiler.cpp', 'gjs/profiler-private.h', 'gjs/text-encoding.cpp', 'gjs/text-encoding.h', + 'gjs/promise.cpp', 'gjs/promise.h', 'gjs/stack.cpp', 'modules/console.cpp', 'modules/console.h', 'modules/modules.cpp', 'modules/modules.h', diff --git a/tools/process_iwyu.py b/tools/process_iwyu.py index 718349b2..2cc32e62 100755 --- a/tools/process_iwyu.py +++ b/tools/process_iwyu.py @@ -106,6 +106,8 @@ FALSE_POSITIVES = ( 'for remove_reference<>::type'), ('gjs/profiler.cpp', '#include <type_traits>', 'for remove_reference<>::type'), + ('gjs/promise.cpp', '#include <type_traits>', + 'for remove_reference<>::type'), ('test/gjs-test-jsapi-utils.cpp', '#include <type_traits>', 'for remove_reference<>::type'), diff --git a/tools/run_iwyu.sh b/tools/run_iwyu.sh index 319da33e..7e1db246 100755 --- a/tools/run_iwyu.sh +++ b/tools/run_iwyu.sh @@ -71,7 +71,7 @@ for FILE in $SRCDIR/gi/*.cpp $SRCDIR/gjs/atoms.cpp $SRCDIR/gjs/byteArray.cpp \ $SRCDIR/gjs/deprecation.cpp $SRCDIR/gjs/error-types.cpp \ $SRCDIR/gjs/engine.cpp $SRCDIR/gjs/global.cpp $SRCDIR/gjs/importer.cpp \ $SRCDIR/gjs/jsapi-util*.cpp $SRCDIR/gjs/module.cpp $SRCDIR/gjs/native.cpp \ - $SRCDIR/gjs/objectbox.cpp $SRCDIR/gjs/stack.cpp \ + $SRCDIR/gjs/objectbox.cpp $SRCDIR/gjs/promise.cpp $SRCDIR/gjs/stack.cpp \ $SRCDIR/modules/cairo-*.cpp $SRCDIR/modules/console.cpp \ $SRCDIR/modules/print.cpp $SRCDIR/modules/system.cpp $SRCDIR/test/*.cpp \ $SRCDIR/util/*.cpp $SRCDIR/libgjs-private/*.c |