CLI: support defining custom commands via C++ & Python API
Add support for add-ons to define commands using the new argument `-c` or `--command`. Commands behave as follows: - Passing in a command enables background mode without the need to pass in `--background`. - All arguments following the command are passed to the command (without the need to use the `--` argument). - Add-ons can define their own commands via `bpy.utils.register_cli_command` (see examples in API docs). - Passing in `--command help` lists all available commands. Ref !119115
This commit is contained in:
parent
5214e6f35d
commit
9372e0dfe0
|
@ -0,0 +1,74 @@
|
|||
"""
|
||||
Using Python Argument Parsing
|
||||
-----------------------------
|
||||
|
||||
This example shows how the Python ``argparse`` module can be used with a custom command.
|
||||
|
||||
Using ``argparse`` is generally recommended as it has many useful utilities and
|
||||
generates a ``--help`` message for your command.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=os.path.basename(sys.argv[0]) + " --command keyconfig_export",
|
||||
description="Write key-configuration to a file.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-o", "--output",
|
||||
dest="output",
|
||||
metavar='OUTPUT',
|
||||
type=str,
|
||||
help="The path to write the keymap to.",
|
||||
required=True,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-a", "--all",
|
||||
dest="all",
|
||||
action="store_true",
|
||||
help="Write all key-maps (not only customized key-maps).",
|
||||
required=False,
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def keyconfig_export(argv):
|
||||
parser = argparse_create()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# Ensure the key configuration is loaded in background mode.
|
||||
bpy.utils.keyconfig_init()
|
||||
|
||||
bpy.ops.preferences.keyconfig_export(
|
||||
filepath=args.output,
|
||||
all=args.all,
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
cli_commands = []
|
||||
|
||||
|
||||
def register():
|
||||
cli_commands.append(bpy.utils.register_cli_command("keyconfig_export", keyconfig_export))
|
||||
|
||||
|
||||
def unregister():
|
||||
for cmd in cli_commands:
|
||||
bpy.utils.unregister_cli_command(cmd)
|
||||
cli_commands.clear()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
|
@ -0,0 +1,43 @@
|
|||
"""
|
||||
Custom Commands
|
||||
---------------
|
||||
|
||||
Registering commands makes it possible to conveniently expose command line
|
||||
functionality via commands passed to (``-c`` / ``--command``).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def sysinfo_command(argv):
|
||||
import tempfile
|
||||
import sys_info
|
||||
|
||||
if argv and argv[0] == "--help":
|
||||
print("Print system information & exit!")
|
||||
return 0
|
||||
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
filepath = os.path.join(tempdir, "system_info.txt")
|
||||
sys_info.write_sysinfo(filepath)
|
||||
with open(filepath, "r", encoding="utf-8") as fh:
|
||||
sys.stdout.write(fh.read())
|
||||
return 0
|
||||
|
||||
|
||||
cli_commands = []
|
||||
|
||||
|
||||
def register():
|
||||
cli_commands.append(bpy.utils.register_cli_command("sysinfo", sysinfo_command))
|
||||
|
||||
|
||||
def unregister():
|
||||
for cmd in cli_commands:
|
||||
bpy.utils.unregister_cli_command(cmd)
|
||||
cli_commands.clear()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
|
@ -21,6 +21,8 @@ __all__ = (
|
|||
"refresh_script_paths",
|
||||
"app_template_paths",
|
||||
"register_class",
|
||||
"register_cli_command",
|
||||
"unregister_cli_command",
|
||||
"register_manual_map",
|
||||
"unregister_manual_map",
|
||||
"register_classes_factory",
|
||||
|
@ -49,9 +51,11 @@ from _bpy import (
|
|||
flip_name,
|
||||
unescape_identifier,
|
||||
register_class,
|
||||
register_cli_command,
|
||||
resource_path,
|
||||
script_paths as _bpy_script_paths,
|
||||
unregister_class,
|
||||
unregister_cli_command,
|
||||
user_resource as _user_resource,
|
||||
system_resource,
|
||||
)
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
struct Main;
|
||||
struct UserDef;
|
||||
struct bContext;
|
||||
|
||||
/**
|
||||
* Only to be called on exit Blender.
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
/** \file
|
||||
* \ingroup bke
|
||||
* \brief Blender CLI Generic `--command` Support.
|
||||
*
|
||||
* \note all registered commands must print help to the STDOUT & exit with a zero exit-code
|
||||
* when `--help` is passed in as the first argument to a command.
|
||||
*/
|
||||
|
||||
#include "BLI_utility_mixins.hh"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Each instance of this class can run the command with an argument list.
|
||||
* The arguments begin at the first argument after the command identifier.
|
||||
*/
|
||||
class CommandHandler : blender::NonCopyable, blender::NonMovable {
|
||||
public:
|
||||
CommandHandler(const std::string &id) : id(id) {}
|
||||
virtual ~CommandHandler() = default;
|
||||
|
||||
/** Matched against `--command {id}`. */
|
||||
const std::string id;
|
||||
|
||||
/**
|
||||
* The main execution function.
|
||||
* The return value is used as the commands exit-code.
|
||||
*/
|
||||
virtual int exec(struct bContext *C, int argc, const char **argv) = 0;
|
||||
|
||||
/** True when one or more registered commands share an ID. */
|
||||
bool is_duplicate = false;
|
||||
};
|
||||
/**
|
||||
* \param cmd: The memory for a command type (ownership is transferred).
|
||||
*/
|
||||
void BKE_blender_cli_command_register(std::unique_ptr<CommandHandler> cmd);
|
||||
|
||||
/**
|
||||
* Unregister a previously registered command.
|
||||
*/
|
||||
bool BKE_blender_cli_command_unregister(CommandHandler *cmd);
|
||||
|
||||
/**
|
||||
* Run the command by `id`, passing in the argument list & context.
|
||||
* The argument list must begin after the command identifier.
|
||||
*/
|
||||
int BKE_blender_cli_command_exec(struct bContext *C,
|
||||
const char *id,
|
||||
const int argc,
|
||||
const char **argv);
|
||||
|
||||
/**
|
||||
* Print all known commands (used for passing `--command help` in the command-line).
|
||||
*/
|
||||
void BKE_blender_cli_command_print_help();
|
||||
/**
|
||||
* Frees all commands (using their #CommandFreeFn call-backs).
|
||||
*/
|
||||
void BKE_blender_cli_command_free_all();
|
|
@ -76,6 +76,7 @@ set(SRC
|
|||
intern/bake_items_serialize.cc
|
||||
intern/bake_items_socket.cc
|
||||
intern/blender.cc
|
||||
intern/blender_cli_command.cc
|
||||
intern/blender_copybuffer.cc
|
||||
intern/blender_undo.cc
|
||||
intern/blender_user_menu.cc
|
||||
|
@ -345,6 +346,7 @@ set(SRC
|
|||
BKE_bake_items_serialize.hh
|
||||
BKE_bake_items_socket.hh
|
||||
BKE_blender.hh
|
||||
BKE_blender_cli_command.hh
|
||||
BKE_blender_copybuffer.hh
|
||||
BKE_blender_undo.hh
|
||||
BKE_blender_user_menu.hh
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup bke
|
||||
*
|
||||
* Generic CLI "--command" declarations.
|
||||
*
|
||||
* Duplicate Commands
|
||||
* ==================
|
||||
*
|
||||
* When two or more commands share the same identifier, a warning is printed and both are disabled.
|
||||
*
|
||||
* This is done because command-line actions may be destructive so the down-side of running the
|
||||
* wrong command could be severe. The reason this is not considered an error is we can't prevent
|
||||
* it so easily, unlike operator ID's which may be longer, commands are typically short terms
|
||||
* which wont necessarily include an add-ons identifier as a prefix for e.g.
|
||||
* Further, an error would break loading add-ons who's primary is *not*
|
||||
* necessarily to provide command-line access.
|
||||
* An alternative solution could be to generate unique names (number them for example)
|
||||
* but this isn't reliable as it would depend on it the order add-ons are loaded which
|
||||
* isn't under user control.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "BKE_blender_cli_command.hh" /* own include */
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Internal API
|
||||
* \{ */
|
||||
|
||||
using CommandHandlerPtr = std::unique_ptr<CommandHandler>;
|
||||
|
||||
/**
|
||||
* All registered command handlers.
|
||||
* \note the order doesn't matter as duplicates are detected and prevented from running.
|
||||
*/
|
||||
blender::Vector<CommandHandlerPtr> g_command_handlers;
|
||||
|
||||
static CommandHandler *blender_cli_command_lookup(const std::string &id)
|
||||
{
|
||||
for (CommandHandlerPtr &cmd_iter : g_command_handlers) {
|
||||
if (id == cmd_iter->id) {
|
||||
return cmd_iter.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static int blender_cli_command_index(const CommandHandler *cmd)
|
||||
{
|
||||
int index = 0;
|
||||
for (CommandHandlerPtr &cmd_iter : g_command_handlers) {
|
||||
if (cmd_iter.get() == cmd) {
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Public API
|
||||
* \{ */
|
||||
|
||||
void BKE_blender_cli_command_register(std::unique_ptr<CommandHandler> cmd)
|
||||
{
|
||||
bool is_duplicate = false;
|
||||
if (CommandHandler *cmd_exists = blender_cli_command_lookup(cmd->id)) {
|
||||
std::cerr << "warning: registered duplicate command \"" << cmd->id
|
||||
<< "\", this will be inaccessible" << std::endl;
|
||||
cmd_exists->is_duplicate = true;
|
||||
is_duplicate = true;
|
||||
}
|
||||
cmd->is_duplicate = is_duplicate;
|
||||
g_command_handlers.append(std::move(cmd));
|
||||
}
|
||||
|
||||
bool BKE_blender_cli_command_unregister(CommandHandler *cmd)
|
||||
{
|
||||
const int cmd_index = blender_cli_command_index(cmd);
|
||||
if (cmd_index == -1) {
|
||||
std::cerr << "failed to unregister command handler" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Update duplicates after removal. */
|
||||
if (cmd->is_duplicate) {
|
||||
CommandHandler *cmd_other = nullptr;
|
||||
for (CommandHandlerPtr &cmd_iter : g_command_handlers) {
|
||||
/* Skip self. */
|
||||
if (cmd == cmd_iter.get()) {
|
||||
continue;
|
||||
}
|
||||
if (cmd_iter->is_duplicate && (cmd_iter->id == cmd->id)) {
|
||||
if (cmd_other) {
|
||||
/* Two or more found, clear and break. */
|
||||
cmd_other = nullptr;
|
||||
break;
|
||||
}
|
||||
cmd_other = cmd_iter.get();
|
||||
}
|
||||
}
|
||||
if (cmd_other) {
|
||||
cmd_other->is_duplicate = false;
|
||||
}
|
||||
}
|
||||
|
||||
g_command_handlers.remove_and_reorder(cmd_index);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int BKE_blender_cli_command_exec(bContext *C, const char *id, const int argc, const char **argv)
|
||||
{
|
||||
CommandHandler *cmd = blender_cli_command_lookup(id);
|
||||
if (cmd == nullptr) {
|
||||
std::cerr << "Unrecognized command: \"" << id << "\"" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (cmd->is_duplicate) {
|
||||
std::cerr << "Command: \"" << id
|
||||
<< "\" was registered multiple times, must be resolved, aborting!" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
return cmd->exec(C, argc, argv);
|
||||
}
|
||||
|
||||
void BKE_blender_cli_command_print_help()
|
||||
{
|
||||
/* As this is isn't ordered sorting in-place is acceptable,
|
||||
* sort alphabetically for display purposes only. */
|
||||
std::sort(g_command_handlers.begin(),
|
||||
g_command_handlers.end(),
|
||||
[](const CommandHandlerPtr &a, const CommandHandlerPtr &b) { return a->id < b->id; });
|
||||
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
std::cout << ((pass == 0) ? "Blender Command Listing:" :
|
||||
"Duplicate Command Listing (ignored):")
|
||||
<< std::endl;
|
||||
|
||||
const bool is_duplicate = pass > 0;
|
||||
bool found = false;
|
||||
bool has_duplicate = false;
|
||||
for (CommandHandlerPtr &cmd_iter : g_command_handlers) {
|
||||
if (cmd_iter->is_duplicate) {
|
||||
has_duplicate = true;
|
||||
}
|
||||
if (cmd_iter->is_duplicate != is_duplicate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::cout << "\t" << cmd_iter->id << std::endl;
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
std::cout << "\tNone found" << std::endl;
|
||||
}
|
||||
/* Don't print that no duplicates are found as it's not helpful. */
|
||||
if (pass == 0 && !has_duplicate) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BKE_blender_cli_command_free_all()
|
||||
{
|
||||
g_command_handlers.clear();
|
||||
}
|
||||
|
||||
/** \} */
|
|
@ -40,6 +40,7 @@ set(SRC
|
|||
bpy_app_translations.cc
|
||||
bpy_app_usd.cc
|
||||
bpy_capi_utils.cc
|
||||
bpy_cli_command.cc
|
||||
bpy_driver.cc
|
||||
bpy_gizmo_wrap.cc
|
||||
bpy_interface.cc
|
||||
|
@ -87,6 +88,7 @@ set(SRC
|
|||
bpy_app_translations.h
|
||||
bpy_app_usd.h
|
||||
bpy_capi_utils.h
|
||||
bpy_cli_command.h
|
||||
bpy_driver.h
|
||||
bpy_gizmo_wrap.h
|
||||
bpy_intern_string.h
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
#include "bpy.h"
|
||||
#include "bpy_app.h"
|
||||
#include "bpy_cli_command.h"
|
||||
#include "bpy_driver.h"
|
||||
#include "bpy_library.h"
|
||||
#include "bpy_operator.h"
|
||||
|
@ -734,6 +735,10 @@ void BPy_init_modules(bContext *C)
|
|||
PYMODULE_ADD_METHOD(mod, &meth_bpy_owner_id_get);
|
||||
PYMODULE_ADD_METHOD(mod, &meth_bpy_owner_id_set);
|
||||
|
||||
/* Register command functions. */
|
||||
PYMODULE_ADD_METHOD(mod, &BPY_cli_command_register_def);
|
||||
PYMODULE_ADD_METHOD(mod, &BPY_cli_command_unregister_def);
|
||||
|
||||
#undef PYMODULE_ADD_METHOD
|
||||
|
||||
/* add our own modules dir, this is a python package */
|
||||
|
|
|
@ -0,0 +1,315 @@
|
|||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup pythonintern
|
||||
*
|
||||
* Wrap `BKE_command_cli_*` to support custom CLI commands.
|
||||
*/
|
||||
#include <Python.h>
|
||||
|
||||
#include "BLI_utildefines.h"
|
||||
|
||||
#include "bpy_capi_utils.h"
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
#include "BKE_blender_cli_command.hh"
|
||||
|
||||
#include "../generic/py_capi_utils.h"
|
||||
#include "../generic/python_compat.h"
|
||||
#include "../generic/python_utildefines.h"
|
||||
|
||||
#include "bpy_cli_command.h" /* Own include. */
|
||||
|
||||
static const char *bpy_cli_command_capsule_name = "bpy_cli_command";
|
||||
static const char *bpy_cli_command_capsule_name_invalid = "bpy_cli_command<invalid>";
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Internal Utilities
|
||||
* \{ */
|
||||
|
||||
/**
|
||||
* Return a list of strings, compatible with the construction of Python's `sys.argv`.
|
||||
*/
|
||||
static PyObject *py_argv_from_bytes(const int argc, const char **argv)
|
||||
{
|
||||
/* Copy functionality from Python's internal `sys.argv` initialization. */
|
||||
PyConfig config;
|
||||
PyConfig_InitPythonConfig(&config);
|
||||
PyStatus status = PyConfig_SetBytesArgv(&config, argc, (char *const *)argv);
|
||||
PyObject *py_argv = nullptr;
|
||||
if (UNLIKELY(PyStatus_Exception(status))) {
|
||||
PyErr_Format(PyExc_ValueError, "%s", status.err_msg);
|
||||
}
|
||||
else {
|
||||
BLI_assert(argc == config.argv.length);
|
||||
py_argv = PyList_New(config.argv.length);
|
||||
for (Py_ssize_t i = 0; i < config.argv.length; i++) {
|
||||
PyList_SET_ITEM(py_argv, i, PyUnicode_FromWideChar(config.argv.items[i], -1));
|
||||
}
|
||||
}
|
||||
PyConfig_Clear(&config);
|
||||
return py_argv;
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Internal Implementation
|
||||
* \{ */
|
||||
|
||||
static int bpy_cli_command_exec(struct bContext *C,
|
||||
PyObject *py_exec_fn,
|
||||
const int argc,
|
||||
const char **argv)
|
||||
{
|
||||
int exit_code = EXIT_FAILURE;
|
||||
PyGILState_STATE gilstate;
|
||||
bpy_context_set(C, &gilstate);
|
||||
|
||||
/* For the most part `sys.argv[-argc:]` is sufficient & less trouble than re-creating this
|
||||
* list. Don't do this because:
|
||||
* - Python scripts *could* have manipulated `sys.argv` (although it's bad practice).
|
||||
* - We may want to support invoking commands directly,
|
||||
* where the arguments aren't necessarily from `sys.argv`.
|
||||
*/
|
||||
bool has_error = false;
|
||||
PyObject *py_argv = py_argv_from_bytes(argc, argv);
|
||||
|
||||
if (py_argv == nullptr) {
|
||||
has_error = true;
|
||||
}
|
||||
else {
|
||||
PyObject *exec_args = PyTuple_New(1);
|
||||
PyTuple_SET_ITEM(exec_args, 0, py_argv);
|
||||
|
||||
PyObject *result = PyObject_Call(py_exec_fn, exec_args, nullptr);
|
||||
|
||||
Py_DECREF(exec_args); /* Frees `py_argv` too. */
|
||||
|
||||
/* Convert `sys.exit` into a return-value.
|
||||
* NOTE: typically `sys.exit` *doesn't* need any special handling,
|
||||
* however it's neater if we use the same code paths for exiting either way. */
|
||||
if ((result == nullptr) && PyErr_ExceptionMatches(PyExc_SystemExit)) {
|
||||
PyObject *error_type, *error_value, *error_traceback;
|
||||
PyErr_Fetch(&error_type, &error_value, &error_traceback);
|
||||
if (PyObject_TypeCheck(error_value, (PyTypeObject *)PyExc_SystemExit) &&
|
||||
(((PySystemExitObject *)error_value)->code != nullptr))
|
||||
{
|
||||
/* When `SystemExit(..)` is raised. */
|
||||
result = ((PySystemExitObject *)error_value)->code;
|
||||
}
|
||||
else {
|
||||
/* When `sys.exit()` is called. */
|
||||
result = error_value;
|
||||
}
|
||||
Py_INCREF(result);
|
||||
PyErr_Restore(error_type, error_value, error_traceback);
|
||||
PyErr_Clear();
|
||||
}
|
||||
|
||||
if (result == nullptr) {
|
||||
has_error = true;
|
||||
}
|
||||
else {
|
||||
if (!PyLong_Check(result)) {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"Expected an int return value, not a %.200s",
|
||||
Py_TYPE(result)->tp_name);
|
||||
has_error = true;
|
||||
}
|
||||
else {
|
||||
const int exit_code_test = PyC_Long_AsI32(result);
|
||||
if ((exit_code_test == -1) && PyErr_Occurred()) {
|
||||
exit_code = EXIT_SUCCESS;
|
||||
has_error = true;
|
||||
}
|
||||
else {
|
||||
exit_code = exit_code_test;
|
||||
}
|
||||
}
|
||||
Py_DECREF(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (has_error) {
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
}
|
||||
|
||||
bpy_context_clear(C, &gilstate);
|
||||
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
static void bpy_cli_command_free(PyObject *py_exec_fn)
|
||||
{
|
||||
/* An explicit unregister clears to avoid acquiring a lock. */
|
||||
if (py_exec_fn) {
|
||||
PyGILState_STATE gilstate = PyGILState_Ensure();
|
||||
Py_DECREF(py_exec_fn);
|
||||
PyGILState_Release(gilstate);
|
||||
}
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Internal Class
|
||||
* \{ */
|
||||
|
||||
class BPyCommandHandler : public CommandHandler {
|
||||
public:
|
||||
BPyCommandHandler(const std::string &id, PyObject *py_exec_fn)
|
||||
: CommandHandler(id), py_exec_fn(py_exec_fn)
|
||||
{
|
||||
}
|
||||
~BPyCommandHandler() override
|
||||
{
|
||||
bpy_cli_command_free(this->py_exec_fn);
|
||||
}
|
||||
|
||||
int exec(struct bContext *C, int argc, const char **argv) override
|
||||
{
|
||||
return bpy_cli_command_exec(C, this->py_exec_fn, argc, argv);
|
||||
}
|
||||
|
||||
PyObject *py_exec_fn = nullptr;
|
||||
};
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Public Methods
|
||||
* \{ */
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_cli_command_register_doc,
|
||||
".. method:: register_cli_command(id, execute)\n"
|
||||
"\n"
|
||||
" Register a command, accessible via the (``-c`` / ``--command``) command-line argument.\n"
|
||||
"\n"
|
||||
" :arg id: The command identifier (must pass an ``str.isidentifier`` check).\n"
|
||||
"\n"
|
||||
" If the ``id`` is already registered, a warning is printed and "
|
||||
"the command is inaccessible to prevent accidents invoking the wrong command."
|
||||
" :type id: str\n"
|
||||
" :arg execute: Callback, taking a single list of strings and returns an int.\n"
|
||||
" The arguments are built from all command-line arguments following the command id.\n"
|
||||
" The return value should be 0 for success, 1 on failure "
|
||||
"(specific error codes from the ``os`` module can also be used).\n"
|
||||
" :type execute: callable\n"
|
||||
" :return: The command handle which can be passed to :func:`unregister_cli_command`.\n"
|
||||
" :rtype: capsule\n");
|
||||
static PyObject *bpy_cli_command_register(PyObject * /*self*/, PyObject *args, PyObject *kw)
|
||||
{
|
||||
PyObject *py_id;
|
||||
PyObject *py_exec_fn;
|
||||
|
||||
static const char *_keywords[] = {
|
||||
"id",
|
||||
"execute",
|
||||
nullptr,
|
||||
};
|
||||
static _PyArg_Parser _parser = {
|
||||
PY_ARG_PARSER_HEAD_COMPAT()
|
||||
"O!" /* `id` */
|
||||
"O" /* `execute` */
|
||||
":register_cli_command",
|
||||
_keywords,
|
||||
nullptr,
|
||||
};
|
||||
if (!_PyArg_ParseTupleAndKeywordsFast(args, kw, &_parser, &PyUnicode_Type, &py_id, &py_exec_fn))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (!PyUnicode_IsIdentifier(py_id)) {
|
||||
PyErr_SetString(PyExc_ValueError, "The command id is not a valid identifier");
|
||||
return nullptr;
|
||||
}
|
||||
if (!PyCallable_Check(py_exec_fn)) {
|
||||
PyErr_SetString(PyExc_ValueError, "The execute argument must be callable");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *id = PyUnicode_AsUTF8(py_id);
|
||||
|
||||
std::unique_ptr<CommandHandler> cmd_ptr = std::make_unique<BPyCommandHandler>(
|
||||
std::string(id), Py_INCREF_RET(py_exec_fn));
|
||||
void *cmd_p = cmd_ptr.get();
|
||||
|
||||
BKE_blender_cli_command_register(std::move(cmd_ptr));
|
||||
|
||||
return PyCapsule_New(cmd_p, bpy_cli_command_capsule_name, nullptr);
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_cli_command_unregister_doc,
|
||||
".. method:: unregister_cli_command(handle)\n"
|
||||
"\n"
|
||||
" Unregister a CLI command.\n"
|
||||
"\n"
|
||||
" :arg handle: The return value of :func:`register_cli_command`.\n"
|
||||
" :type handle: capsule\n");
|
||||
static PyObject *bpy_cli_command_unregister(PyObject * /*self*/, PyObject *value)
|
||||
{
|
||||
if (!PyCapsule_CheckExact(value)) {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"Expected a capsule returned from register_cli_command(...), found a: %.200s",
|
||||
Py_TYPE(value)->tp_name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BPyCommandHandler *cmd = static_cast<BPyCommandHandler *>(
|
||||
PyCapsule_GetPointer(value, bpy_cli_command_capsule_name));
|
||||
if (cmd == nullptr) {
|
||||
const char *capsule_name = PyCapsule_GetName(value);
|
||||
if (capsule_name == bpy_cli_command_capsule_name_invalid) {
|
||||
PyErr_SetString(PyExc_ValueError, "The command has already been removed");
|
||||
}
|
||||
else {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Unrecognized capsule ID \"%.200s\"",
|
||||
capsule_name ? capsule_name : "<null>");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Don't acquire the GIL when un-registering. */
|
||||
Py_CLEAR(cmd->py_exec_fn);
|
||||
|
||||
/* Don't allow removing again. */
|
||||
PyCapsule_SetName(value, bpy_cli_command_capsule_name_invalid);
|
||||
|
||||
BKE_blender_cli_command_unregister((CommandHandler *)cmd);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
#if (defined(__GNUC__) && !defined(__clang__))
|
||||
# pragma GCC diagnostic push
|
||||
# pragma GCC diagnostic ignored "-Wcast-function-type"
|
||||
#endif
|
||||
|
||||
PyMethodDef BPY_cli_command_register_def = {
|
||||
"register_cli_command",
|
||||
(PyCFunction)bpy_cli_command_register,
|
||||
METH_STATIC | METH_VARARGS | METH_KEYWORDS,
|
||||
bpy_cli_command_register_doc,
|
||||
};
|
||||
PyMethodDef BPY_cli_command_unregister_def = {
|
||||
"unregister_cli_command",
|
||||
(PyCFunction)bpy_cli_command_unregister,
|
||||
METH_STATIC | METH_O,
|
||||
bpy_cli_command_unregister_doc,
|
||||
};
|
||||
|
||||
#if (defined(__GNUC__) && !defined(__clang__))
|
||||
# pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
/** \} */
|
|
@ -0,0 +1,20 @@
|
|||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup pythonintern
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern PyMethodDef BPY_cli_command_register_def;
|
||||
extern PyMethodDef BPY_cli_command_unregister_def;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -51,6 +51,7 @@
|
|||
|
||||
#include "BKE_addon.h"
|
||||
#include "BKE_appdir.hh"
|
||||
#include "BKE_blender_cli_command.hh"
|
||||
#include "BKE_mask.h" /* free mask clipboard */
|
||||
#include "BKE_material.h" /* BKE_material_copybuf_clear */
|
||||
#include "BKE_studiolight.h"
|
||||
|
@ -535,6 +536,14 @@ void WM_exit_ex(bContext *C, const bool do_python_exit, const bool do_user_exit_
|
|||
}
|
||||
#endif
|
||||
|
||||
/* Perform this early in case commands reference other data freed later in this function.
|
||||
* This most run:
|
||||
* - After add-ons are disabled because they may unregister commands.
|
||||
* - Before Python exits so Python objects can be de-referenced.
|
||||
* - Before #BKE_blender_atexit runs they free the `argv` on WIN32.
|
||||
*/
|
||||
BKE_blender_cli_command_free_all();
|
||||
|
||||
BLI_timer_free();
|
||||
|
||||
WM_paneltype_clear();
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
/* Mostly initialization functions. */
|
||||
#include "BKE_appdir.hh"
|
||||
#include "BKE_blender.hh"
|
||||
#include "BKE_blender_cli_command.hh"
|
||||
#include "BKE_brush.hh"
|
||||
#include "BKE_cachefile.hh"
|
||||
#include "BKE_callbacks.hh"
|
||||
|
@ -566,8 +567,23 @@ int main(int argc,
|
|||
|
||||
#ifndef WITH_PYTHON_MODULE
|
||||
if (G.background) {
|
||||
int exit_code;
|
||||
if (app_state.command.argv) {
|
||||
const char *id = app_state.command.argv[0];
|
||||
if (STREQ(id, "help")) {
|
||||
BKE_blender_cli_command_print_help();
|
||||
exit_code = EXIT_SUCCESS;
|
||||
}
|
||||
else {
|
||||
exit_code = BKE_blender_cli_command_exec(
|
||||
C, id, app_state.command.argc - 1, app_state.command.argv + 1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
exit_code = G.is_break ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||
}
|
||||
/* Using window-manager API in background-mode is a bit odd, but works fine. */
|
||||
WM_exit(C, G.is_break ? EXIT_FAILURE : EXIT_SUCCESS);
|
||||
WM_exit(C, exit_code);
|
||||
}
|
||||
else {
|
||||
/* Shows the splash as needed. */
|
||||
|
|
|
@ -698,6 +698,8 @@ static void print_help(bArgs *ba, bool all)
|
|||
PRINT("\n");
|
||||
BLI_args_print_arg_doc(ba, "-noaudio");
|
||||
BLI_args_print_arg_doc(ba, "-setaudio");
|
||||
PRINT("\n");
|
||||
BLI_args_print_arg_doc(ba, "--command");
|
||||
|
||||
PRINT("\n");
|
||||
|
||||
|
@ -924,6 +926,32 @@ static int arg_handle_background_mode_set(int /*argc*/, const char ** /*argv*/,
|
|||
return 0;
|
||||
}
|
||||
|
||||
static const char arg_handle_command_set_doc[] =
|
||||
"<command>\n"
|
||||
"\tRun a command which consumes all remaining arguments.\n"
|
||||
"\tUse '-c help' to list all other commands.\n"
|
||||
"\tPass '--help' after the command to see its help text.\n"
|
||||
"\n"
|
||||
"\tThis implies '--background' mode.";
|
||||
static int arg_handle_command_set(int argc, const char **argv, void * /*data*/)
|
||||
{
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "%s requires at least one argument\n", argv[0]);
|
||||
exit(EXIT_FAILURE);
|
||||
BLI_assert_unreachable();
|
||||
}
|
||||
|
||||
/* See `--background` implementation. */
|
||||
G.background = true;
|
||||
BKE_sound_force_device("None");
|
||||
|
||||
app_state.command.argc = argc - 1;
|
||||
app_state.command.argv = argv + 1;
|
||||
|
||||
/* Consume remaining arguments. */
|
||||
return argc - 1;
|
||||
}
|
||||
|
||||
static const char arg_handle_log_level_set_doc[] =
|
||||
"<level>\n"
|
||||
"\tSet the logging verbosity level (higher for more details) defaults to 1,\n"
|
||||
|
@ -2374,6 +2402,8 @@ void main_args_setup(bContext *C, bArgs *ba, bool all)
|
|||
ba, nullptr, "--disable-abort-handler", CB(arg_handle_abort_handler_disable), nullptr);
|
||||
|
||||
BLI_args_add(ba, "-b", "--background", CB(arg_handle_background_mode_set), nullptr);
|
||||
/* Command implies background mode. */
|
||||
BLI_args_add(ba, "-c", "--command", CB(arg_handle_command_set), nullptr);
|
||||
|
||||
BLI_args_add(ba, "-a", nullptr, CB(arg_handle_playback_mode), nullptr);
|
||||
|
||||
|
|
|
@ -52,6 +52,12 @@ struct ApplicationState {
|
|||
struct {
|
||||
unsigned char python;
|
||||
} exit_code_on_error;
|
||||
|
||||
/** Storage for commands (see `--command` argument). */
|
||||
struct {
|
||||
int argc;
|
||||
const char **argv;
|
||||
} command;
|
||||
};
|
||||
extern struct ApplicationState app_state; /* creator.c */
|
||||
|
||||
|
|
Loading…
Reference in New Issue