95806c6a58
When adapting the check to the new versioning check, we just blindly copied the old behavior for stable/unstable versions: - stable releases must have matching major numbers - unstable releases must match major and minor ("alpha", "beta", "rc") That worked for the old even/odd scheme, but now has the absurd effect that we consider an extension that lists "40.alpha" in its shell-version incompatible with "40.beta", but compatible with "40.2". At least this provides us with a good opportunity to reconsider the behavior. While it is true that breakage is much more likely between unstable releases, in practice extensions are either following shell development closely or update once around the time of a stable release. For the former, the stricter check isn't usually too useful (as the extension releases around the same time as gnome-shell anyway). For the latter, it's annoying that ".rc" is treated differently from ".0" and requires an update to become compatible. The latter is also by far the more common case, so update the check to only match on the major version regardless of whether a release is stable or unstable. https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/3787 Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1719>
234 lines
6.8 KiB
JavaScript
234 lines
6.8 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/* exported ExtensionState, ExtensionType, getCurrentExtension,
|
|
getSettings, initTranslations, openPrefs, isOutOfDate,
|
|
installImporter, serializeExtension, deserializeExtension */
|
|
|
|
// Common utils for the extension system and the extension
|
|
// preferences tool
|
|
|
|
const { Gio, GLib } = imports.gi;
|
|
|
|
const Gettext = imports.gettext;
|
|
|
|
const Config = imports.misc.config;
|
|
|
|
var ExtensionType = {
|
|
SYSTEM: 1,
|
|
PER_USER: 2,
|
|
};
|
|
|
|
var ExtensionState = {
|
|
ENABLED: 1,
|
|
DISABLED: 2,
|
|
ERROR: 3,
|
|
OUT_OF_DATE: 4,
|
|
DOWNLOADING: 5,
|
|
INITIALIZED: 6,
|
|
|
|
// Used as an error state for operations on unknown extensions,
|
|
// should never be in a real extensionMeta object.
|
|
UNINSTALLED: 99,
|
|
};
|
|
|
|
const SERIALIZED_PROPERTIES = [
|
|
'type',
|
|
'state',
|
|
'path',
|
|
'error',
|
|
'hasPrefs',
|
|
'hasUpdate',
|
|
'canChange',
|
|
];
|
|
|
|
/**
|
|
* getCurrentExtension:
|
|
*
|
|
* @returns {?object} - The current extension, or null if not called from
|
|
* an extension.
|
|
*/
|
|
function getCurrentExtension() {
|
|
let stack = new Error().stack.split('\n');
|
|
let extensionStackLine;
|
|
|
|
// Search for an occurrence of an extension stack frame
|
|
// Start at 1 because 0 is the stack frame of this function
|
|
for (let i = 1; i < stack.length; i++) {
|
|
if (stack[i].includes('/gnome-shell/extensions/')) {
|
|
extensionStackLine = stack[i];
|
|
break;
|
|
}
|
|
}
|
|
if (!extensionStackLine)
|
|
return null;
|
|
|
|
// The stack line is like:
|
|
// init([object Object])@/home/user/data/gnome-shell/extensions/u@u.id/prefs.js:8
|
|
//
|
|
// In the case that we're importing from
|
|
// module scope, the first field is blank:
|
|
// @/home/user/data/gnome-shell/extensions/u@u.id/prefs.js:8
|
|
let match = new RegExp('@(.+):\\d+').exec(extensionStackLine);
|
|
if (!match)
|
|
return null;
|
|
|
|
// local import, as the module is used from outside the gnome-shell process
|
|
// as well (not this function though)
|
|
let extensionManager = imports.ui.main.extensionManager;
|
|
|
|
let path = match[1];
|
|
let file = Gio.File.new_for_path(path);
|
|
|
|
// Walk up the directory tree, looking for an extension with
|
|
// the same UUID as a directory name.
|
|
while (file != null) {
|
|
let extension = extensionManager.lookup(file.get_basename());
|
|
if (extension !== undefined)
|
|
return extension;
|
|
file = file.get_parent();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* initTranslations:
|
|
* @param {string=} domain - the gettext domain to use
|
|
*
|
|
* Initialize Gettext to load translations from extensionsdir/locale.
|
|
* If @domain is not provided, it will be taken from metadata['gettext-domain']
|
|
*/
|
|
function initTranslations(domain) {
|
|
let extension = getCurrentExtension();
|
|
|
|
if (!extension)
|
|
throw new Error('initTranslations() can only be called from extensions');
|
|
|
|
domain = domain || extension.metadata['gettext-domain'];
|
|
|
|
// Expect USER extensions to have a locale/ subfolder, otherwise assume a
|
|
// SYSTEM extension that has been installed in the same prefix as the shell
|
|
let localeDir = extension.dir.get_child('locale');
|
|
if (localeDir.query_exists(null))
|
|
Gettext.bindtextdomain(domain, localeDir.get_path());
|
|
else
|
|
Gettext.bindtextdomain(domain, Config.LOCALEDIR);
|
|
}
|
|
|
|
/**
|
|
* getSettings:
|
|
* @param {string=} schema - the GSettings schema id
|
|
* @returns {Gio.Settings} - a new settings object for @schema
|
|
*
|
|
* Builds and returns a GSettings schema for @schema, using schema files
|
|
* in extensionsdir/schemas. If @schema is omitted, it is taken from
|
|
* metadata['settings-schema'].
|
|
*/
|
|
function getSettings(schema) {
|
|
let extension = getCurrentExtension();
|
|
|
|
if (!extension)
|
|
throw new Error('getSettings() can only be called from extensions');
|
|
|
|
schema = schema || extension.metadata['settings-schema'];
|
|
|
|
const GioSSS = Gio.SettingsSchemaSource;
|
|
|
|
// Expect USER extensions to have a schemas/ subfolder, otherwise assume a
|
|
// SYSTEM extension that has been installed in the same prefix as the shell
|
|
let schemaDir = extension.dir.get_child('schemas');
|
|
let schemaSource;
|
|
if (schemaDir.query_exists(null)) {
|
|
schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
|
|
GioSSS.get_default(),
|
|
false);
|
|
} else {
|
|
schemaSource = GioSSS.get_default();
|
|
}
|
|
|
|
let schemaObj = schemaSource.lookup(schema, true);
|
|
if (!schemaObj)
|
|
throw new Error(`Schema ${schema} could not be found for extension ${extension.metadata.uuid}. Please check your installation`);
|
|
|
|
return new Gio.Settings({ settings_schema: schemaObj });
|
|
}
|
|
|
|
/**
|
|
* openPrefs:
|
|
*
|
|
* Open the preference dialog of the current extension
|
|
*/
|
|
function openPrefs() {
|
|
const extension = getCurrentExtension();
|
|
|
|
if (!extension)
|
|
throw new Error('openPrefs() can only be called from extensions');
|
|
|
|
try {
|
|
const extensionManager = imports.ui.main.extensionManager;
|
|
extensionManager.openExtensionPrefs(extension.uuid, '', {});
|
|
} catch (e) {
|
|
if (e.name === 'ImportError')
|
|
throw new Error('openPrefs() cannot be called from preferences');
|
|
logError(e, 'Failed to open extension preferences');
|
|
}
|
|
}
|
|
|
|
function isOutOfDate(extension) {
|
|
const [major] = Config.PACKAGE_VERSION.split('.');
|
|
return !extension.metadata['shell-version'].some(v => v.startsWith(major));
|
|
}
|
|
|
|
function serializeExtension(extension) {
|
|
let obj = { ...extension.metadata };
|
|
|
|
SERIALIZED_PROPERTIES.forEach(prop => {
|
|
obj[prop] = extension[prop];
|
|
});
|
|
|
|
let res = {};
|
|
for (let key in obj) {
|
|
let val = obj[key];
|
|
let type;
|
|
switch (typeof val) {
|
|
case 'string':
|
|
type = 's';
|
|
break;
|
|
case 'number':
|
|
type = 'd';
|
|
break;
|
|
case 'boolean':
|
|
type = 'b';
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
res[key] = GLib.Variant.new(type, val);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
function deserializeExtension(variant) {
|
|
let res = { metadata: {} };
|
|
for (let prop in variant) {
|
|
let val = variant[prop].unpack();
|
|
if (SERIALIZED_PROPERTIES.includes(prop))
|
|
res[prop] = val;
|
|
else
|
|
res.metadata[prop] = val;
|
|
}
|
|
// add the 2 additional properties to create a valid extension object, as createExtensionObject()
|
|
res.uuid = res.metadata.uuid;
|
|
res.dir = Gio.File.new_for_path(res.path);
|
|
return res;
|
|
}
|
|
|
|
function installImporter(extension) {
|
|
let oldSearchPath = imports.searchPath.slice(); // make a copy
|
|
imports.searchPath = [extension.dir.get_parent().get_path()];
|
|
// importing a "subdir" creates a new importer object that doesn't affect
|
|
// the global one
|
|
extension.imports = imports[extension.uuid];
|
|
imports.searchPath = oldSearchPath;
|
|
}
|