Files
test/source/blender/blenkernel/intern/preferences.cc
Campbell Barton dc9430c480 Extensions: support system repositories & BLENDER_SYSTEM_EXTENSIONS
Support for "System" extensions as an alternative to the current
"User" extensions repository.

The purpose of this change is to support bundling extensions for
offline work or in environments where users setting up thier own
extensions isn't desirable, see #122512.

Details:

The default "System" repository on Linux will for example use:
- `/usr/share/blender/4.2/extensions/{system}` For system installs.
- `./4.2/extensions/{system}` For portable installs.

- Blender's default startup now has a "System" repository
  which users or administrators may populate.

- Repositories can select between User/System paths,
  setting a custom path overrides overrides this setting.

- Add "BLENDER_SYSTEM_EXTENSIONS" (matching "BLENDER_LOCAL_EXTENSIONS").

Ref !122832
2024-06-07 11:36:20 +10:00

514 lines
16 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bke
*
* User defined asset library API.
*/
#include <cstring>
#include "MEM_guardedalloc.h"
#include "BLI_fileops.h"
#include "BLI_listbase.h"
#include "BLI_path_util.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLI_string_utils.hh"
#include "BKE_appdir.hh"
#include "BKE_asset.hh"
#include "BKE_preferences.h"
#include "BLT_translation.hh"
#include "BLO_read_write.hh"
#include "DNA_asset_types.h"
#include "DNA_defaults.h"
#include "DNA_userdef_types.h"
#define U BLI_STATIC_ASSERT(false, "Global 'U' not allowed, only use arguments passed in!")
/* -------------------------------------------------------------------- */
/** \name Asset Libraries
* \{ */
bUserAssetLibrary *BKE_preferences_asset_library_add(UserDef *userdef,
const char *name,
const char *dirpath)
{
bUserAssetLibrary *library = DNA_struct_default_alloc(bUserAssetLibrary);
BLI_addtail(&userdef->asset_libraries, library);
if (name) {
BKE_preferences_asset_library_name_set(userdef, library, name);
}
if (dirpath) {
STRNCPY(library->dirpath, dirpath);
}
return library;
}
void BKE_preferences_asset_library_remove(UserDef *userdef, bUserAssetLibrary *library)
{
BLI_freelinkN(&userdef->asset_libraries, library);
}
void BKE_preferences_asset_library_name_set(UserDef *userdef,
bUserAssetLibrary *library,
const char *name)
{
STRNCPY_UTF8(library->name, name);
BLI_uniquename(&userdef->asset_libraries,
library,
name,
'.',
offsetof(bUserAssetLibrary, name),
sizeof(library->name));
}
void BKE_preferences_asset_library_path_set(bUserAssetLibrary *library, const char *path)
{
STRNCPY(library->dirpath, path);
if (BLI_is_file(library->dirpath)) {
BLI_path_parent_dir(library->dirpath);
}
}
bUserAssetLibrary *BKE_preferences_asset_library_find_index(const UserDef *userdef, int index)
{
return static_cast<bUserAssetLibrary *>(BLI_findlink(&userdef->asset_libraries, index));
}
bUserAssetLibrary *BKE_preferences_asset_library_find_by_name(const UserDef *userdef,
const char *name)
{
return static_cast<bUserAssetLibrary *>(
BLI_findstring(&userdef->asset_libraries, name, offsetof(bUserAssetLibrary, name)));
}
bUserAssetLibrary *BKE_preferences_asset_library_containing_path(const UserDef *userdef,
const char *path)
{
LISTBASE_FOREACH (bUserAssetLibrary *, asset_lib_pref, &userdef->asset_libraries) {
if (BLI_path_contains(asset_lib_pref->dirpath, path)) {
return asset_lib_pref;
}
}
return nullptr;
}
int BKE_preferences_asset_library_get_index(const UserDef *userdef,
const bUserAssetLibrary *library)
{
return BLI_findindex(&userdef->asset_libraries, library);
}
void BKE_preferences_asset_library_default_add(UserDef *userdef)
{
char documents_path[FILE_MAXDIR];
/* No home or documents path found, not much we can do. */
if (!BKE_appdir_folder_documents(documents_path) || !documents_path[0]) {
return;
}
bUserAssetLibrary *library = BKE_preferences_asset_library_add(
userdef, DATA_(BKE_PREFS_ASSET_LIBRARY_DEFAULT_NAME), nullptr);
/* Add new "Default" library under '[doc_path]/Blender/Assets'. */
BLI_path_join(
library->dirpath, sizeof(library->dirpath), documents_path, N_("Blender"), N_("Assets"));
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Extension Repositories
* \{ */
/**
* A string copy that ensures: `[A-Za-z]+[A-Za-z0-9_]*`.
*/
static size_t strncpy_py_module(char *dst, const char *src, const size_t dst_maxncpy)
{
const size_t dst_len_max = dst_maxncpy - 1;
dst[0] = '\0';
size_t i_src = 0, i_dst = 0;
while (src[i_src] && (i_dst < dst_len_max)) {
const char c = src[i_src++];
const bool is_alpha = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
/* The first character must be `[a-zA-Z]`. */
if (i_dst == 0 && !is_alpha) {
continue;
}
const bool is_num = (is_alpha == false) && ((c >= '0' && c <= '9') || c == '_');
if (!(is_alpha || is_num)) {
continue;
}
dst[i_dst++] = c;
}
dst[i_dst] = '\0';
return i_dst;
}
bUserExtensionRepo *BKE_preferences_extension_repo_add(UserDef *userdef,
const char *name,
const char *module,
const char *custom_dirpath)
{
bUserExtensionRepo *repo = DNA_struct_default_alloc(bUserExtensionRepo);
BLI_addtail(&userdef->extension_repos, repo);
/* Set the unique ID-name. */
BKE_preferences_extension_repo_name_set(userdef, repo, name);
/* Set the unique module-name. */
BKE_preferences_extension_repo_module_set(userdef, repo, module);
/* Set the directory. */
STRNCPY(repo->custom_dirpath, custom_dirpath);
BLI_path_normalize(repo->custom_dirpath);
BLI_path_slash_rstrip(repo->custom_dirpath);
/* While not a strict rule, ignored paths that already exist, *
* pointing to the same path is going to logical problems with package-management. */
LISTBASE_FOREACH (const bUserExtensionRepo *, repo_iter, &userdef->extension_repos) {
if (repo == repo_iter) {
continue;
}
if (BLI_path_cmp(repo->custom_dirpath, repo_iter->custom_dirpath) == 0) {
repo->custom_dirpath[0] = '\0';
break;
}
}
return repo;
}
void BKE_preferences_extension_repo_remove(UserDef *userdef, bUserExtensionRepo *repo)
{
BLI_freelinkN(&userdef->extension_repos, repo);
}
bUserExtensionRepo *BKE_preferences_extension_repo_add_default_remote(UserDef *userdef)
{
bUserExtensionRepo *repo = BKE_preferences_extension_repo_add(
userdef, "extensions.blender.org", "blender_org", "");
/* The trailing slash on this URL is important, without it a redirect is used. */
STRNCPY(repo->remote_url, "https://extensions.blender.org/api/v1/extensions/");
/* Disable `blender.org` by default, the initial "Online Preferences" section gives
* the option to enable this. */
repo->flag |= USER_EXTENSION_REPO_FLAG_USE_REMOTE_URL | USER_EXTENSION_REPO_FLAG_SYNC_ON_STARTUP;
return repo;
}
bUserExtensionRepo *BKE_preferences_extension_repo_add_default_user(UserDef *userdef)
{
bUserExtensionRepo *repo = BKE_preferences_extension_repo_add(
userdef, "User Default", "user_default", "");
return repo;
}
bUserExtensionRepo *BKE_preferences_extension_repo_add_default_system(UserDef *userdef)
{
bUserExtensionRepo *repo = BKE_preferences_extension_repo_add(userdef, "System", "system", "");
repo->source = USER_EXTENSION_REPO_SOURCE_SYSTEM;
return repo;
}
void BKE_preferences_extension_repo_add_defaults_all(UserDef *userdef)
{
BLI_assert(BLI_listbase_is_empty(&userdef->extension_repos));
BKE_preferences_extension_repo_add_default_remote(userdef);
BKE_preferences_extension_repo_add_default_user(userdef);
BKE_preferences_extension_repo_add_default_system(userdef);
}
void BKE_preferences_extension_repo_name_set(UserDef *userdef,
bUserExtensionRepo *repo,
const char *name)
{
if (*name == '\0') {
name = "User Repository";
}
STRNCPY_UTF8(repo->name, name);
BLI_uniquename(&userdef->extension_repos,
repo,
name,
'.',
offsetof(bUserExtensionRepo, name),
sizeof(repo->name));
}
void BKE_preferences_extension_repo_module_set(UserDef *userdef,
bUserExtensionRepo *repo,
const char *module)
{
if (strncpy_py_module(repo->module, module, sizeof(repo->module)) == 0) {
STRNCPY(repo->module, "repository");
}
BLI_uniquename(&userdef->extension_repos,
repo,
module,
'_',
offsetof(bUserExtensionRepo, module),
sizeof(repo->module));
}
void BKE_preferences_extension_repo_custom_dirpath_set(bUserExtensionRepo *repo, const char *path)
{
STRNCPY(repo->custom_dirpath, path);
}
size_t BKE_preferences_extension_repo_dirpath_get(const bUserExtensionRepo *repo,
char *dirpath,
const int dirpath_maxncpy)
{
if (repo->flag & USER_EXTENSION_REPO_FLAG_USE_CUSTOM_DIRECTORY) {
return BLI_strncpy_rlen(dirpath, repo->custom_dirpath, dirpath_maxncpy);
}
std::optional<std::string> path = std::nullopt;
switch (repo->source) {
case USER_EXTENSION_REPO_SOURCE_SYSTEM: {
path = BKE_appdir_folder_id(BLENDER_SYSTEM_EXTENSIONS, nullptr);
break;
}
default: { /* #USER_EXTENSION_REPO_SOURCE_USER. */
path = BKE_appdir_folder_id_user_notest(BLENDER_USER_EXTENSIONS, nullptr);
break;
}
}
/* Highly unlikely to fail as the directory doesn't have to exist. */
if (!path) {
dirpath[0] = '\0';
return 0;
}
return BLI_path_join(dirpath, dirpath_maxncpy, path.value().c_str(), repo->module);
}
bUserExtensionRepo *BKE_preferences_extension_repo_find_index(const UserDef *userdef, int index)
{
return static_cast<bUserExtensionRepo *>(BLI_findlink(&userdef->extension_repos, index));
}
bUserExtensionRepo *BKE_preferences_extension_repo_find_by_module(const UserDef *userdef,
const char *module)
{
return static_cast<bUserExtensionRepo *>(
BLI_findstring(&userdef->extension_repos, module, offsetof(bUserExtensionRepo, module)));
}
static bool url_char_is_delimiter(const char ch)
{
/* Punctuation (space to comma). */
if (ch >= 32 && ch <= 44) {
return true;
}
/* Other characters (colon to at-sign). */
if (ch >= 58 && ch <= 64) {
return true;
}
if (ELEM(ch, '/', '\\')) {
return true;
}
return false;
}
bUserExtensionRepo *BKE_preferences_extension_repo_find_by_remote_url_prefix(
const UserDef *userdef, const char *remote_url_full, const bool only_enabled)
{
const int path_full_len = strlen(remote_url_full);
const int path_full_offset = BKE_preferences_extension_repo_remote_scheme_end(remote_url_full);
LISTBASE_FOREACH (bUserExtensionRepo *, repo, &userdef->extension_repos) {
if (only_enabled && (repo->flag & USER_EXTENSION_REPO_FLAG_DISABLED)) {
continue;
}
/* Has a valid remote path to check. */
if ((repo->flag & USER_EXTENSION_REPO_FLAG_USE_REMOTE_URL) == 0) {
continue;
}
if (repo->remote_url[0] == '\0') {
continue;
}
/* Set path variables which may be offset by the "scheme". */
const char *path_repo = repo->remote_url;
const char *path_test = remote_url_full;
int path_test_len = path_full_len;
/* Allow paths beginning with both `http` & `https` to be considered equivalent.
* This is done by skipping the "scheme" prefix both have a scheme. */
if (path_full_offset) {
const int path_repo_offset = BKE_preferences_extension_repo_remote_scheme_end(path_repo);
if (path_repo_offset) {
path_repo += path_repo_offset;
path_test += path_full_offset;
path_test_len -= path_full_offset;
}
}
/* The length of the path without trailing slashes. */
int path_repo_len = strlen(path_repo);
while (path_repo_len && ELEM(path_repo[path_repo_len - 1], '/', '\\')) {
path_repo_len--;
}
if (path_test_len <= path_repo_len) {
continue;
}
if (memcmp(path_repo, path_test, path_repo_len) != 0) {
continue;
}
/* A delimiter must follow to ensure `path_test` doesn't reference a longer host-name.
* Will typically be a `/` or a `:`. */
if (!url_char_is_delimiter(path_test[path_repo_len])) {
continue;
}
return repo;
}
return nullptr;
}
int BKE_preferences_extension_repo_remote_scheme_end(const char *url)
{
/* Technically the "://" are not part of the scheme, so subtract 3 from the return value. */
const char *scheme_check[] = {
"http://",
"https://",
"file://",
};
for (int i = 0; i < ARRAY_SIZE(scheme_check); i++) {
const char *scheme = scheme_check[i];
int scheme_len = strlen(scheme);
if (strncmp(url, scheme, scheme_len) == 0) {
return scheme_len - 3;
}
}
return 0;
}
void BKE_preferences_extension_remote_to_name(const char *remote_url,
char name[sizeof(bUserExtensionRepo::name)])
{
name[0] = '\0';
if (int offset = BKE_preferences_extension_repo_remote_scheme_end(remote_url)) {
remote_url += (offset + 3);
}
if (UNLIKELY(remote_url[0] == '\0')) {
return;
}
const char *c = remote_url;
/* Skip any delimiters (likely forward slashes for `file:///` on UNIX). */
while (*c && url_char_is_delimiter(*c)) {
c++;
}
/* Skip the domain name. */
while (*c && !url_char_is_delimiter(*c)) {
c++;
}
BLI_strncpy_utf8(
name, remote_url, std::min(size_t(c - remote_url) + 1, sizeof(bUserExtensionRepo::name)));
}
int BKE_preferences_extension_repo_get_index(const UserDef *userdef,
const bUserExtensionRepo *repo)
{
return BLI_findindex(&userdef->extension_repos, repo);
}
void BKE_preferences_extension_repo_read_data(BlendDataReader *reader, bUserExtensionRepo *repo)
{
if (repo->access_token) {
BLO_read_string(reader, &repo->access_token);
}
}
void BKE_preferences_extension_repo_write_data(BlendWriter *writer, const bUserExtensionRepo *repo)
{
if (repo->access_token) {
BLO_write_string(writer, repo->access_token);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #bUserAssetShelfSettings
* \{ */
static bUserAssetShelfSettings *asset_shelf_settings_new(UserDef *userdef,
const char *shelf_idname)
{
bUserAssetShelfSettings *settings = DNA_struct_default_alloc(bUserAssetShelfSettings);
BLI_addtail(&userdef->asset_shelves_settings, settings);
STRNCPY(settings->shelf_idname, shelf_idname);
BLI_assert(BLI_listbase_is_empty(&settings->enabled_catalog_paths));
return settings;
}
static bUserAssetShelfSettings *asset_shelf_settings_ensure(UserDef *userdef,
const char *shelf_idname)
{
if (bUserAssetShelfSettings *settings = BKE_preferences_asset_shelf_settings_get(userdef,
shelf_idname))
{
return settings;
}
return asset_shelf_settings_new(userdef, shelf_idname);
}
bUserAssetShelfSettings *BKE_preferences_asset_shelf_settings_get(const UserDef *userdef,
const char *shelf_idname)
{
return static_cast<bUserAssetShelfSettings *>(
BLI_findstring(&userdef->asset_shelves_settings,
shelf_idname,
offsetof(bUserAssetShelfSettings, shelf_idname)));
}
bool BKE_preferences_asset_shelf_settings_is_catalog_path_enabled(const UserDef *userdef,
const char *shelf_idname,
const char *catalog_path)
{
const bUserAssetShelfSettings *settings = BKE_preferences_asset_shelf_settings_get(userdef,
shelf_idname);
if (!settings) {
return false;
}
return BKE_asset_catalog_path_list_has_path(settings->enabled_catalog_paths, catalog_path);
}
bool BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(UserDef *userdef,
const char *shelf_idname,
const char *catalog_path)
{
if (BKE_preferences_asset_shelf_settings_is_catalog_path_enabled(
userdef, shelf_idname, catalog_path))
{
return false;
}
bUserAssetShelfSettings *settings = asset_shelf_settings_ensure(userdef, shelf_idname);
BKE_asset_catalog_path_list_add_path(settings->enabled_catalog_paths, catalog_path);
return true;
}
/** \} */