summaryrefslogtreecommitdiff
path: root/modules/script/package.js
blob: 1491b063978b9962c8add7155dd3c7cd019f397b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
// SPDX-FileCopyrightText: 2012 Giovanni Campagna
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later

/* exported checkSymbol, datadir, init, initFormat, initGettext, initSubmodule,
libdir, localedir, moduledir, name, pkgdatadir, pkglibdir, prefix, require,
requireSymbol, run, start, version */

/**
 * This module provides a set of convenience APIs for building packaged
 * applications.
 */

const GLib = imports.gi.GLib;
const GIRepository = imports.gi.GIRepository;
const Gio = imports.gi.Gio;
const GObject = imports.gi.GObject;
const System = imports.system;

const Gettext = imports.gettext;

// public
var name;
var version;
var prefix;
var datadir;
var libdir;
var pkgdatadir;
var pkglibdir;
var moduledir;
var localedir;

// private
let _pkgname;
let _base;
let _submoduledir;

function _findEffectiveEntryPointName() {
    let entryPoint = System.programInvocationName;
    while (GLib.file_test(entryPoint, GLib.FileTest.IS_SYMLINK))
        entryPoint = GLib.file_read_link(entryPoint);

    return GLib.path_get_basename(entryPoint);
}

function _runningFromSource() {
    let binary = Gio.File.new_for_path(System.programInvocationName);
    let sourceBinary = Gio.File.new_for_path(`./src/${name}`);
    return binary.equal(sourceBinary);
}

function _runningFromMesonSource() {
    return GLib.getenv('MESON_BUILD_ROOT') &&
           GLib.getenv('MESON_SOURCE_ROOT');
}

function _makeNamePath(n) {
    return `/${n.replace(/\./g, '/')}`;
}

/**
 * Initialize directories and global variables. Must be called
 * before any of other API in Package is used.
 * `params` must be an object with at least the following keys:
 *  - name: the package name ($(PACKAGE_NAME) in autotools,
 *          eg. org.foo.Bar)
 *  - version: the package version
 *  - prefix: the installation prefix
 *
 * init() will take care to check if the program is running from
 * the source directory or not, by looking for a 'src' directory.
 *
 * At the end, the global variable 'pkg' will contain the
 * Package module (imports.package). Additionally, the following
 * module variables will be available:
 *  - name: the base name of the entry point (eg. org.foo.Bar.App)
 *  - version: same as in @params
 *  - prefix: the installation prefix (as passed in @params)
 *  - datadir, libdir: the final datadir and libdir when installed;
 *                     usually, these would be prefix + '/share' and
 *                     and prefix + '/lib' (or '/lib64')
 *  - pkgdatadir: the directory to look for private data files, such as
 *                images, stylesheets and UI definitions;
 *                this will be datadir + name when installed and
 *                './data' when running from the source tree
 *  - pkglibdir: the directory to look for private typelibs and C
 *               libraries;
 *               this will be libdir + name when installed and
 *               './lib' when running from the source tree
 *  - moduledir: the directory to look for JS modules;
 *               this will be pkglibdir when installed and
 *               './src' when running from the source tree
 *  - localedir: the directory containing gettext translation files;
 *               this will be datadir + '/locale' when installed
 *               and './po' in the source tree
 *
 * All paths are absolute and will not end with '/'.
 *
 * As a side effect, init() calls GLib.set_prgname().
 *
 * @param {object} params package parameters
 */
function init(params) {
    globalThis.pkg = imports.package;
    _pkgname = params.name;
    name = _findEffectiveEntryPointName();
    version = params.version;

    // Must call it first, because it can only be called
    // once, and other library calls might have it as a
    // side effect
    GLib.set_prgname(name);

    prefix = params.prefix;
    libdir = params.libdir;
    datadir = GLib.build_filenamev([prefix, 'share']);
    let libpath, girpath;

    if (_runningFromMesonSource()) {
        log('Running from Meson, using local files');
        let bld = GLib.getenv('MESON_BUILD_ROOT');
        let src = GLib.getenv('MESON_SOURCE_ROOT');

        pkglibdir = libpath = girpath = GLib.build_filenamev([bld, 'lib']);
        pkgdatadir = GLib.build_filenamev([bld, 'data']);
        localedir = GLib.build_filenamev([bld, 'po']);
        _submoduledir = GLib.build_filenamev([bld, 'subprojects']);

        GLib.setenv('GSETTINGS_SCHEMA_DIR', pkgdatadir, true);
        try {
            let resource = Gio.Resource.load(GLib.build_filenamev([bld, 'src',
                `${name}.src.gresource`]));
            resource._register();
            moduledir = `resource://${_makeNamePath(name)}/js`;
        } catch (e) {
            moduledir = GLib.build_filenamev([src, 'src']);
        }
    } else if (_runningFromSource()) {
        log('Running from source tree, using local files');
        // Running from source directory
        _base = GLib.get_current_dir();
        _submoduledir = _base;
        pkglibdir = GLib.build_filenamev([_base, 'lib']);
        libpath = GLib.build_filenamev([pkglibdir, '.libs']);
        girpath = pkglibdir;
        pkgdatadir = GLib.build_filenamev([_base, 'data']);
        localedir = GLib.build_filenamev([_base, 'po']);
        moduledir = GLib.build_filenamev([_base, 'src']);

        GLib.setenv('GSETTINGS_SCHEMA_DIR', pkgdatadir, true);
    } else {
        _base = prefix;
        pkglibdir = GLib.build_filenamev([libdir, _pkgname]);
        libpath = pkglibdir;
        girpath = GLib.build_filenamev([pkglibdir, 'girepository-1.0']);
        pkgdatadir = GLib.build_filenamev([datadir, _pkgname]);
        localedir = GLib.build_filenamev([datadir, 'locale']);

        try {
            let resource = Gio.Resource.load(GLib.build_filenamev([pkgdatadir,
                `${name}.src.gresource`]));
            resource._register();

            moduledir = `resource://${_makeNamePath(name)}/js`;
        } catch (e) {
            moduledir = pkgdatadir;
        }
    }

    imports.searchPath.unshift(moduledir);
    GIRepository.Repository.prepend_search_path(girpath);
    GIRepository.Repository.prepend_library_path(libpath);

    try {
        let resource = Gio.Resource.load(GLib.build_filenamev([pkgdatadir,
            `${name}.data.gresource`]));
        resource._register();
    } catch (e) { }
}

/**
 * This is a convenience function if your package has a
 * single entry point.
 * You must define a main(ARGV) function inside a main.js
 * module in moduledir.
 *
 * @param {object} params see init()
 */
function start(params) {
    init(params);
    run(imports.main);
}

/**
 * This is the function to use if you want to have multiple
 * entry points in one package.
 * You must define a main(ARGV) function inside the passed
 * in module, and then the launcher would be
 *
 * imports.package.init(...);
 * imports.package.run(imports.entrypoint);
 *
 * @param {object} module the module to run
 * @returns {number|undefined} the exit code of the module's main() function
 */
function run(module) {
    return module.main([System.programInvocationName].concat(ARGV));
}

/**
 * Mark a dependency on a specific version of one or more
 * external GI typelibs.
 * `libs` must be an object whose keys are a typelib name,
 * and values are the respective version. The empty string
 * indicates any version.
 *
 * @param {object} libs the external dependencies to import
 */
function require(libs) {
    for (let l in libs)
        requireSymbol(l, libs[l]);
}

/**
 * As checkSymbol(), but exit with an error if the
 * dependency cannot be satisfied.
 *
 * @param {string} lib an external dependency to import
 * @param {string} [ver] version of the dependency
 * @param {string} [symbol] symbol to check for
 */
function requireSymbol(lib, ver, symbol) {
    if (!checkSymbol(lib, ver, symbol)) {
        if (symbol)
            printerr(`Unsatisfied dependency: No ${symbol} in ${lib}`);
        else
            printerr(`Unsatisfied dependency: ${lib}`);
        System.exit(1);
    }
}

/**
 * Check whether an external GI typelib can be imported
 * and provides @symbol.
 *
 * Symbols may refer to
 *  - global functions         ('main_quit')
 *  - classes                  ('Window')
 *  - class / instance methods ('IconTheme.get_default' / 'IconTheme.has_icon')
 *  - GObject properties       ('Window.default_height')
 *
 * @param {string} lib an external dependency to import
 * @param {string} [ver] version of the dependency
 * @param {string} [symbol] symbol to check for
 * @returns {boolean} true if `lib` can be imported and provides `symbol`, false
 * otherwise
 */
function checkSymbol(lib, ver, symbol) {
    let Lib = null;

    if (ver)
        imports.gi.versions[lib] = ver;

    try {
        Lib = imports.gi[lib];
    } catch (e) {
        return false;
    }

    if (!symbol)
        return true; // Done

    let [klass, sym] = symbol.split('.');
    if (klass === symbol)
        return typeof Lib[symbol] !== 'undefined';

    let obj = Lib[klass];
    if (typeof obj === 'undefined')
        return false;

    if (typeof obj[sym] !== 'undefined' ||
        obj.prototype && typeof obj.prototype[sym] !== 'undefined')
        return true; // class- or object method

    // GObject property
    let pspec = null;
    if (GObject.type_is_a(obj.$gtype, GObject.TYPE_INTERFACE)) {
        let iface = GObject.type_default_interface_ref(obj.$gtype);
        pspec = GObject.Object.interface_find_property(iface, sym);
    } else if (GObject.type_is_a(obj.$gtype, GObject.TYPE_OBJECT)) {
        pspec = GObject.Object.find_property.call(obj.$gtype, sym);
    }

    return pspec !== null;
}

function initGettext() {
    Gettext.bindtextdomain(_pkgname, localedir);
    Gettext.textdomain(_pkgname);

    let gettext = imports.gettext;
    globalThis._ = gettext.gettext;
    globalThis.C_ = gettext.pgettext;
    globalThis.N_ = function (x) {
        return x;
    };
}

function initFormat() {
    // eslint-disable-next-line no-restricted-properties
    let format = imports.format;
    String.prototype.format = format.format;
}

function initSubmodule(moduleName) {
    if (_runningFromMesonSource() || _runningFromSource()) {
        // Running from source tree, add './moduleName' to search paths

        let submoduledir = GLib.build_filenamev([_submoduledir, moduleName]);
        let libpath;
        if (_runningFromMesonSource())
            libpath = submoduledir;
        else
            libpath = GLib.build_filenamev([submoduledir, '.libs']);
        GIRepository.Repository.prepend_search_path(submoduledir);
        GIRepository.Repository.prepend_library_path(libpath);
    } else {
        // Running installed, submodule is in $(pkglibdir), nothing to do
    }
}