зеркало из https://github.com/mozilla/gecko-dev.git
bug 1498909 - dynamically load libsecret at runtime if available r=franziskus,jcj
Enough linux-based systems don't have libsecret that we can't make it a requirement on linux. For those that do, however, we can dynamically load the library at runtime. For those that don't, we can fall back to NSS. Differential Revision: https://phabricator.services.mozilla.com/D9969 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
012ea52a90
Коммит
74e6b5cabe
|
@ -1350,20 +1350,3 @@ if CONFIG['MOZ_WAYLAND']:
|
|||
'wayland-egl.h',
|
||||
'wayland-util.h',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_LIB_SECRET']:
|
||||
system_headers += [
|
||||
'libsecret/secret.h',
|
||||
'libsecret/secret-attributes.h',
|
||||
'libsecret/secret-collection.h',
|
||||
'libsecret/secret-enum-types.h',
|
||||
'libsecret/secret-item.h',
|
||||
'libsecret/secret-password.h',
|
||||
'libsecret/secret-paths.h',
|
||||
'libsecret/secret-prompt.h',
|
||||
'libsecret/secret-schema.h',
|
||||
'libsecret/secret-schemas.h',
|
||||
'libsecret/secret-types.h',
|
||||
'libsecret/secret-value.h',
|
||||
'libsecret/secret-service.h',
|
||||
]
|
||||
|
|
|
@ -6,9 +6,12 @@
|
|||
|
||||
#include "LibSecret.h"
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <gmodule.h>
|
||||
#include <memory>
|
||||
|
||||
#include "mozilla/Base64.h"
|
||||
#include "prlink.h"
|
||||
|
||||
// This is the implementation of LibSecret, an instantiation of OSKeyStore for
|
||||
// Linux.
|
||||
|
@ -17,9 +20,180 @@ using namespace mozilla;
|
|||
|
||||
LazyLogModule gLibSecretLog("libsecret");
|
||||
|
||||
static PRLibrary* libsecret = nullptr;
|
||||
|
||||
typedef struct _SecretService SecretService;
|
||||
typedef struct _SecretCollection SecretCollection;
|
||||
|
||||
typedef enum {
|
||||
SECRET_SCHEMA_NONE = 0,
|
||||
SECRET_SCHEMA_DONT_MATCH_NAME = 1 << 1
|
||||
} SecretSchemaFlags;
|
||||
|
||||
typedef enum {
|
||||
SECRET_SCHEMA_ATTRIBUTE_STRING = 0,
|
||||
SECRET_SCHEMA_ATTRIBUTE_INTEGER = 1,
|
||||
SECRET_SCHEMA_ATTRIBUTE_BOOLEAN = 2,
|
||||
} SecretSchemaAttributeType;
|
||||
|
||||
typedef struct {
|
||||
const gchar* name;
|
||||
SecretSchemaAttributeType type;
|
||||
} SecretSchemaAttribute;
|
||||
|
||||
typedef struct {
|
||||
const gchar* name;
|
||||
SecretSchemaFlags flags;
|
||||
SecretSchemaAttribute attributes[32];
|
||||
|
||||
/* <private> */
|
||||
gint reserved;
|
||||
gpointer reserved1;
|
||||
gpointer reserved2;
|
||||
gpointer reserved3;
|
||||
gpointer reserved4;
|
||||
gpointer reserved5;
|
||||
gpointer reserved6;
|
||||
gpointer reserved7;
|
||||
} SecretSchema;
|
||||
|
||||
typedef enum {
|
||||
SECRET_COLLECTION_NONE = 0 << 0,
|
||||
SECRET_COLLECTION_LOAD_ITEMS = 1 << 1,
|
||||
} SecretCollectionFlags;
|
||||
|
||||
typedef enum {
|
||||
SECRET_SERVICE_NONE = 0,
|
||||
SECRET_SERVICE_OPEN_SESSION = 1 << 1,
|
||||
SECRET_SERVICE_LOAD_COLLECTIONS = 1 << 2,
|
||||
} SecretServiceFlags;
|
||||
|
||||
typedef enum {
|
||||
SECRET_ERROR_PROTOCOL = 1,
|
||||
SECRET_ERROR_IS_LOCKED = 2,
|
||||
SECRET_ERROR_NO_SUCH_OBJECT = 3,
|
||||
SECRET_ERROR_ALREADY_EXISTS = 4,
|
||||
} SecretError;
|
||||
|
||||
#define SECRET_COLLECTION_DEFAULT "default"
|
||||
|
||||
typedef SecretCollection* (*secret_collection_for_alias_sync_fn)(
|
||||
SecretService*, const gchar*, SecretCollectionFlags, GCancellable*,
|
||||
GError**);
|
||||
typedef SecretService* (*secret_service_get_sync_fn)(SecretServiceFlags,
|
||||
GCancellable*, GError**);
|
||||
typedef gint (*secret_service_lock_sync_fn)(SecretService*, GList*,
|
||||
GCancellable*, GList**, GError**);
|
||||
typedef gboolean (*secret_password_clear_sync_fn)(const SecretSchema*,
|
||||
GCancellable*, GError**, ...);
|
||||
typedef gchar* (*secret_password_lookup_sync_fn)(const SecretSchema*,
|
||||
GCancellable*, GError**, ...);
|
||||
typedef gboolean (*secret_password_store_sync_fn)(const SecretSchema*,
|
||||
const gchar*, const gchar*,
|
||||
const gchar*, GCancellable*,
|
||||
GError**, ...);
|
||||
typedef void (*secret_password_free_fn)(const gchar*);
|
||||
typedef GQuark (*secret_error_get_quark_fn)();
|
||||
|
||||
static secret_collection_for_alias_sync_fn secret_collection_for_alias_sync =
|
||||
nullptr;
|
||||
static secret_service_get_sync_fn secret_service_get_sync = nullptr;
|
||||
static secret_service_lock_sync_fn secret_service_lock_sync = nullptr;
|
||||
static secret_password_clear_sync_fn secret_password_clear_sync = nullptr;
|
||||
static secret_password_lookup_sync_fn secret_password_lookup_sync = nullptr;
|
||||
static secret_password_store_sync_fn secret_password_store_sync = nullptr;
|
||||
static secret_password_free_fn secret_password_free = nullptr;
|
||||
static secret_error_get_quark_fn secret_error_get_quark = nullptr;
|
||||
|
||||
nsresult MaybeLoadLibSecret() {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
if (!NS_IsMainThread()) {
|
||||
return NS_ERROR_NOT_SAME_THREAD;
|
||||
}
|
||||
|
||||
if (!libsecret) {
|
||||
libsecret = PR_LoadLibrary("libsecret-1.so.0");
|
||||
if (!libsecret) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
#define FIND_FUNCTION_SYMBOL(function) \
|
||||
function = (function##_fn)PR_FindFunctionSymbol(libsecret, #function); \
|
||||
if (!(function)) { \
|
||||
PR_UnloadLibrary(libsecret); \
|
||||
libsecret = nullptr; \
|
||||
return NS_ERROR_NOT_AVAILABLE; \
|
||||
}
|
||||
FIND_FUNCTION_SYMBOL(secret_collection_for_alias_sync);
|
||||
FIND_FUNCTION_SYMBOL(secret_service_get_sync);
|
||||
FIND_FUNCTION_SYMBOL(secret_service_lock_sync);
|
||||
FIND_FUNCTION_SYMBOL(secret_password_clear_sync);
|
||||
FIND_FUNCTION_SYMBOL(secret_password_lookup_sync);
|
||||
FIND_FUNCTION_SYMBOL(secret_password_store_sync);
|
||||
FIND_FUNCTION_SYMBOL(secret_password_free);
|
||||
FIND_FUNCTION_SYMBOL(secret_error_get_quark);
|
||||
#undef FIND_FUNCTION_SYMBOL
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
struct ScopedDelete {
|
||||
void operator()(SecretService* ss) {
|
||||
if (ss) g_object_unref(ss);
|
||||
}
|
||||
void operator()(SecretCollection* sc) {
|
||||
if (sc) g_object_unref(sc);
|
||||
}
|
||||
void operator()(GError* error) {
|
||||
if (error) g_error_free(error);
|
||||
}
|
||||
void operator()(GList* list) {
|
||||
if (list) g_list_free(list);
|
||||
}
|
||||
void operator()(char* val) {
|
||||
if (val) secret_password_free(val);
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct ScopedMaybeDelete {
|
||||
void operator()(T* ptr) {
|
||||
if (ptr) {
|
||||
ScopedDelete del;
|
||||
del(ptr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::unique_ptr<GError, ScopedMaybeDelete<GError>> ScopedGError;
|
||||
typedef std::unique_ptr<GList, ScopedMaybeDelete<GList>> ScopedGList;
|
||||
typedef std::unique_ptr<char, ScopedMaybeDelete<char>> ScopedPassword;
|
||||
typedef std::unique_ptr<SecretCollection, ScopedMaybeDelete<SecretCollection>>
|
||||
ScopedSecretCollection;
|
||||
typedef std::unique_ptr<SecretService, ScopedMaybeDelete<SecretService>>
|
||||
ScopedSecretService;
|
||||
|
||||
LibSecret::LibSecret() {}
|
||||
|
||||
LibSecret::~LibSecret() {}
|
||||
LibSecret::~LibSecret() {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
if (!NS_IsMainThread()) {
|
||||
return;
|
||||
}
|
||||
if (libsecret) {
|
||||
secret_collection_for_alias_sync = nullptr;
|
||||
secret_service_get_sync = nullptr;
|
||||
secret_service_lock_sync = nullptr;
|
||||
secret_password_clear_sync = nullptr;
|
||||
secret_password_lookup_sync = nullptr;
|
||||
secret_password_store_sync = nullptr;
|
||||
secret_password_free = nullptr;
|
||||
secret_error_get_quark = nullptr;
|
||||
PR_UnloadLibrary(libsecret);
|
||||
libsecret = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static const SecretSchema kSchema = {
|
||||
"mozilla.firefox",
|
||||
|
@ -29,6 +203,10 @@ static const SecretSchema kSchema = {
|
|||
|
||||
nsresult GetScopedServices(ScopedSecretService& aSs,
|
||||
ScopedSecretCollection& aSc) {
|
||||
MOZ_ASSERT(secret_service_get_sync && secret_collection_for_alias_sync);
|
||||
if (!secret_service_get_sync || !secret_collection_for_alias_sync) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
GError* raw_error = nullptr;
|
||||
aSs = ScopedSecretService(secret_service_get_sync(
|
||||
static_cast<SecretServiceFlags>(
|
||||
|
@ -55,6 +233,10 @@ nsresult GetScopedServices(ScopedSecretService& aSs,
|
|||
}
|
||||
|
||||
nsresult LibSecret::Lock() {
|
||||
MOZ_ASSERT(secret_service_lock_sync);
|
||||
if (!secret_service_lock_sync) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
ScopedSecretService ss;
|
||||
ScopedSecretCollection sc;
|
||||
if (NS_FAILED(GetScopedServices(ss, sc))) {
|
||||
|
@ -90,10 +272,22 @@ nsresult LibSecret::Unlock() {
|
|||
|
||||
nsresult LibSecret::StoreSecret(const nsACString& aSecret,
|
||||
const nsACString& aLabel) {
|
||||
MOZ_ASSERT(secret_password_store_sync);
|
||||
if (!secret_password_store_sync) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
// libsecret expects a null-terminated string, so to be safe we store the
|
||||
// secret (which could be arbitrary bytes) base64-encoded.
|
||||
nsAutoCString base64;
|
||||
nsresult rv = Base64Encode(aSecret, base64);
|
||||
if (NS_FAILED(rv)) {
|
||||
MOZ_LOG(gLibSecretLog, LogLevel::Debug, ("Error base64-encoding secret"));
|
||||
return rv;
|
||||
}
|
||||
GError* raw_error = nullptr;
|
||||
bool stored = secret_password_store_sync(
|
||||
&kSchema, SECRET_COLLECTION_DEFAULT, PromiseFlatCString(aLabel).get(),
|
||||
PromiseFlatCString(aSecret).get(),
|
||||
PromiseFlatCString(base64).get(),
|
||||
nullptr, // GCancellable
|
||||
&raw_error, "string", PromiseFlatCString(aLabel).get(), nullptr);
|
||||
ScopedGError error(raw_error);
|
||||
|
@ -106,22 +300,31 @@ nsresult LibSecret::StoreSecret(const nsACString& aSecret,
|
|||
}
|
||||
|
||||
nsresult LibSecret::DeleteSecret(const nsACString& aLabel) {
|
||||
MOZ_ASSERT(secret_password_clear_sync && secret_error_get_quark);
|
||||
if (!secret_password_clear_sync || !secret_error_get_quark) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
GError* raw_error = nullptr;
|
||||
bool r = secret_password_clear_sync(
|
||||
Unused << secret_password_clear_sync(
|
||||
&kSchema,
|
||||
nullptr, // GCancellable
|
||||
&raw_error, "string", PromiseFlatCString(aLabel).get(), nullptr);
|
||||
ScopedGError error(raw_error);
|
||||
if (raw_error) {
|
||||
if (raw_error && !(raw_error->domain == secret_error_get_quark() &&
|
||||
raw_error->code == SECRET_ERROR_NO_SUCH_OBJECT)) {
|
||||
MOZ_LOG(gLibSecretLog, LogLevel::Debug, ("Error deleting secret"));
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
return r ? NS_OK : NS_ERROR_FAILURE;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult LibSecret::RetrieveSecret(const nsACString& aLabel,
|
||||
/* out */ nsACString& aSecret) {
|
||||
MOZ_ASSERT(secret_password_lookup_sync && secret_password_free);
|
||||
if (!secret_password_lookup_sync || !secret_password_free) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
GError* raw_error = nullptr;
|
||||
aSecret.Truncate();
|
||||
ScopedPassword s(secret_password_lookup_sync(
|
||||
|
@ -134,7 +337,15 @@ nsresult LibSecret::RetrieveSecret(const nsACString& aLabel,
|
|||
("Error retrieving secret or didn't find it"));
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
aSecret.Assign(s.get(), strlen(s.get()));
|
||||
// libsecret expects a null-terminated string, so to be safe we store the
|
||||
// secret (which could be arbitrary bytes) base64-encoded, which means we have
|
||||
// to base64-decode it here.
|
||||
nsAutoCString base64Encoded(s.get());
|
||||
nsresult rv = Base64Decode(base64Encoded, aSecret);
|
||||
if (NS_FAILED(rv)) {
|
||||
MOZ_LOG(gLibSecretLog, LogLevel::Debug, ("Error base64-decoding secret"));
|
||||
return rv;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
|
|
@ -4,66 +4,14 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef MOZ_LIB_SECRET
|
||||
#error LibSecret OSKeyStore included when MOZ_LIB_SECRET is not defined!
|
||||
#endif
|
||||
|
||||
#ifndef LibSecret_h
|
||||
#define LibSecret_h
|
||||
|
||||
#include "OSKeyStore.h"
|
||||
|
||||
#include <libsecret/secret.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "nsString.h"
|
||||
|
||||
struct ScopedDelete {
|
||||
void operator()(SecretService* ss) {
|
||||
if (ss) g_object_unref(ss);
|
||||
}
|
||||
void operator()(SecretCollection* sc) {
|
||||
if (sc) g_object_unref(sc);
|
||||
}
|
||||
void operator()(GError* error) {
|
||||
if (error) g_error_free(error);
|
||||
}
|
||||
void operator()(GList* list) {
|
||||
if (list) g_list_free(list);
|
||||
}
|
||||
void operator()(SecretValue* val) {
|
||||
if (val) secret_value_unref(val);
|
||||
}
|
||||
void operator()(SecretItem* val) {
|
||||
if (val) g_object_unref(val);
|
||||
}
|
||||
void operator()(char* val) {
|
||||
if (val) secret_password_free(val);
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct ScopedMaybeDelete {
|
||||
void operator()(T* ptr) {
|
||||
if (ptr) {
|
||||
ScopedDelete del;
|
||||
del(ptr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#define SCOPED(x) typedef std::unique_ptr<x, ScopedMaybeDelete<x>> Scoped##x
|
||||
|
||||
SCOPED(SecretService);
|
||||
SCOPED(SecretCollection);
|
||||
SCOPED(GError);
|
||||
SCOPED(GList);
|
||||
SCOPED(SecretValue);
|
||||
SCOPED(SecretItem);
|
||||
typedef std::unique_ptr<char, ScopedMaybeDelete<char>> ScopedPassword;
|
||||
|
||||
#undef SCOPED
|
||||
nsresult MaybeLoadLibSecret();
|
||||
|
||||
class LibSecret final : public AbstractOSKeyStore {
|
||||
public:
|
||||
|
|
|
@ -12,12 +12,13 @@
|
|||
#include "nsXPCOM.h"
|
||||
#include "pk11pub.h"
|
||||
|
||||
#ifdef MOZ_LIB_SECRET
|
||||
#include "LibSecret.h"
|
||||
#elif defined(XP_MACOSX)
|
||||
#if defined(XP_MACOSX)
|
||||
#include "KeychainSecret.h"
|
||||
#elif defined(XP_WIN)
|
||||
#include "CredentialManagerSecret.h"
|
||||
#elif defined(MOZ_WIDGET_GTK)
|
||||
#include "LibSecret.h"
|
||||
#include "NSSKeyStore.h"
|
||||
#else
|
||||
#include "NSSKeyStore.h"
|
||||
#endif
|
||||
|
@ -27,8 +28,6 @@ NS_IMPL_ISUPPORTS(OSKeyStore, nsIOSKeyStore, nsIObserver)
|
|||
using namespace mozilla;
|
||||
using dom::Promise;
|
||||
|
||||
mozilla::LazyLogModule gOSKeyStoreLog("oskeystore");
|
||||
|
||||
OSKeyStore::OSKeyStore()
|
||||
: mKs(nullptr), mKsThread(nullptr), mKsIsNSSKeyStore(false) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
@ -36,12 +35,17 @@ OSKeyStore::OSKeyStore()
|
|||
return;
|
||||
}
|
||||
|
||||
#ifdef MOZ_LIB_SECRET
|
||||
mKs.reset(new LibSecret());
|
||||
#elif defined(XP_MACOSX)
|
||||
#if defined(XP_MACOSX)
|
||||
mKs.reset(new KeychainSecret());
|
||||
#elif defined(XP_WIN)
|
||||
mKs.reset(new CredentialManagerSecret());
|
||||
#elif defined(MOZ_WIDGET_GTK)
|
||||
if (NS_SUCCEEDED(MaybeLoadLibSecret())) {
|
||||
mKs.reset(new LibSecret());
|
||||
} else {
|
||||
mKs.reset(new NSSKeyStore());
|
||||
mKsIsNSSKeyStore = true;
|
||||
}
|
||||
#else
|
||||
mKs.reset(new NSSKeyStore());
|
||||
mKsIsNSSKeyStore = true;
|
||||
|
|
|
@ -141,12 +141,12 @@ UNIFIED_SOURCES += [
|
|||
'TransportSecurityInfo.cpp',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_LIB_SECRET']:
|
||||
if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
|
||||
UNIFIED_SOURCES += [
|
||||
'LibSecret.cpp',
|
||||
]
|
||||
CFLAGS += CONFIG['MOZ_LIB_SECRET_CFLAGS']
|
||||
CXXFLAGS += CONFIG['MOZ_LIB_SECRET_CFLAGS']
|
||||
CFLAGS += CONFIG['GLIB_CFLAGS']
|
||||
CXXFLAGS += CONFIG['GLIB_CFLAGS']
|
||||
|
||||
if CONFIG['OS_ARCH'] == 'Darwin':
|
||||
UNIFIED_SOURCES += [
|
||||
|
|
|
@ -161,3 +161,48 @@ add_task(async function() {
|
|||
|
||||
await delete_all_secrets();
|
||||
});
|
||||
|
||||
// Test that using a recovery phrase works.
|
||||
add_task(async function() {
|
||||
await delete_all_secrets();
|
||||
|
||||
let keystore = Cc["@mozilla.org/security/oskeystore;1"]
|
||||
.getService(Ci.nsIOSKeyStore);
|
||||
|
||||
let recoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
|
||||
ok(recoveryPhrase, "A recovery phrase should've been created.");
|
||||
|
||||
let text = new Uint8Array([0x01, 0x00, 0x01]);
|
||||
let ciphertext = await keystore.asyncEncryptBytes(LABELS[0], text.length, text);
|
||||
ok(ciphertext, "We should have a ciphertext now.");
|
||||
|
||||
await keystore.asyncDeleteSecret(LABELS[0]);
|
||||
// Decrypting should fail after deleting the secret.
|
||||
await keystore.asyncDecryptBytes(LABELS[0], ciphertext)
|
||||
.then(() => ok(false, "decrypting didn't throw as expected after deleting the secret"))
|
||||
.catch(() => ok(true, "decrypting threw as expected after deleting the secret"));
|
||||
|
||||
await keystore.asyncRecoverSecret(LABELS[0], recoveryPhrase);
|
||||
let plaintext = await keystore.asyncDecryptBytes(LABELS[0], ciphertext);
|
||||
ok(plaintext.toString() == text.toString(), "Decrypted plaintext should be the same as text.");
|
||||
|
||||
await delete_all_secrets();
|
||||
});
|
||||
|
||||
// Test that trying to use a non-base64 recovery phrase fails.
|
||||
add_task(async function() {
|
||||
await delete_all_secrets();
|
||||
|
||||
let keystore = Cc["@mozilla.org/security/oskeystore;1"]
|
||||
.getService(Ci.nsIOSKeyStore);
|
||||
await keystore.asyncRecoverSecret(LABELS[0], "@##$^&*()#$^&*(@#%&*_")
|
||||
.then(() => ok(false, "base64-decoding non-base64 should have failed but didn't"))
|
||||
.catch(() => ok(true, "base64-decoding non-base64 failed as expected"));
|
||||
|
||||
ok(!await keystore.asyncSecretAvailable(LABELS[0]),
|
||||
"we didn't recover a secret, so the secret shouldn't be available");
|
||||
let recoveryPhrase = await keystore.asyncGenerateSecret(LABELS[0]);
|
||||
ok(recoveryPhrase && recoveryPhrase.length > 0,
|
||||
"we should be able to re-use that label to generate a new secret");
|
||||
await delete_all_secrets();
|
||||
});
|
||||
|
|
|
@ -215,9 +215,6 @@ if CONFIG['MOZ_ANDROID_GOOGLE_VR']:
|
|||
OS_LIBS += CONFIG['MOZ_CAIRO_OSLIBS']
|
||||
OS_LIBS += CONFIG['MOZ_WEBRTC_X11_LIBS']
|
||||
|
||||
if CONFIG['MOZ_LIB_SECRET']:
|
||||
OS_LIBS += CONFIG['MOZ_LIB_SECRET_LIBS']
|
||||
|
||||
if CONFIG['MOZ_SYSTEM_JPEG']:
|
||||
OS_LIBS += CONFIG['MOZ_JPEG_LIBS']
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче