GpgFrontend Modules
This directory is a sub-repository of the main GpgFrontend project and must
live at GpgFrontend/modules. It cannot be built standalone: toolchains, SDK
headers, Qt setup, and output paths all come from the superproject's CMake. The
modules are built alongside the application, packaged into signed .gfmodule
descriptors, and loaded at startup. You can disable individual modules in the
application settings, or remove one from an archive install by deleting its
namespace directory under the modules root (see
Packaging, Signing & Distribution).
Integrated Modules
| Directory | Module ID | Description |
|---|---|---|
m_ver_check |
…module.version_checking |
Checks for new GpgFrontend releases. |
m_gpg_info |
…module.gnupg_info_gathering |
Displays local GnuPG installation details. |
m_key_server_sync |
…module.key_server_sync |
Searches and syncs keys with HKP/VKS servers. |
m_email |
…module.email |
E-mail helpers built on vmime. |
m_pgp_inspect |
…module.pgp_inspect |
Inspects the packet structure of OpenPGP data. |
m_im |
…module.im |
OpenPGP messages as single-line chat tokens. |
Repository Layout
modules/
├── src/
│ ├── m_ver_check/ # Version checking module
│ ├── m_gpg_info/ # GnuPG info gathering module
│ ├── m_key_server_sync/ # Key server sync module
│ ├── m_email/ # E-mail module
│ ├── m_pgp_inspect/ # OpenPGP structure inspector
│ ├── m_im/ # Instant messaging tokens
│ └── CMakeLists.txt
└── CMakeLists.txt
Each module directory holds a module.json (its signed identity — see
Writing a New Module) beside its CMakeLists.txt.
The module-facing SDK is not in this sub-repository. Two layers live in the main tree:
src/sdk/— the public SDK (gf_sdk): theGFSDK*functions and structs a module calls directly (GFGpgEncrypt,GFStorageStateGetText, …), or through thegf::sdk::C++ conveniences inGFSDK.hpp. Stateless — every call that needs the host takes an explicit context argument. Seesrc/sdk/README.mdfor the full architecture.src/module_runtime/include/—GFModule.hand friends, statically linked into every module. This is what a module author actually includes: it supplies the event/result types, memory helpers, logging macros, and the one bootstrap function (GFModuleGetApi) that negotiates the ABI with the host and dispatches events to your hooks.
Both are added to the include path automatically by gf_add_module().
Build Notes
- Enable modules by setting
-DGPGFRONTEND_BUILD_MODULES=ONin the superproject CMake configuration. - Modules require Qt 6.5 or later, the same minimum as the host.
CMAKE_AUTOMOC,CMAKE_AUTORCC, andCMAKE_AUTOUICare enabled automatically for all modules.- Translations, packaging, and signing are all handled by
gf_add_module()— see below. There is nothing left to wire up by hand in a module's ownCMakeLists.txt. - Translations use the shared locale set: it is declared once as
GPGFRONTEND_SUPPORTED_LOCALESincmake/Translations.cmake, and.tsfiles live under each module'sts/subdirectory, named after itstranslation_context. Runscripts/update_translations.shto create/refresh the.tsfiles for the app and every module at once.
Writing a New Module
1. Create the directory
modules/src/my_module/
Add add_subdirectory(my_module) to modules/src/CMakeLists.txt.
2. Declare the module's identity in module.json
{
"schema_version": 1,
"id": "com.example.my_module",
"version": "1.0.0",
"name": "MyModule",
"description": "What this module does.",
"author": "Your Name",
"capabilities": ["ui"],
"events": ["APPLICATION_LOADED"],
"commands": ["com.example.my_module.show_about"],
"translation_context": "ModuleMyModule"
}
An optional "min_host_version" field pins the oldest GpgFrontend release the
module requires; it defaults to the version of GpgFrontend it was built
against.
This file is the single source of truth for the module's identity: it feeds the generated C++ identity header, the translation wiring, and the signed package manifest, so none of those can drift apart.
events is an allowlist, enforced by the host: it refuses to subscribe a
packaged module to an event its signed manifest does not name, and refuses
any id this host never fires at all. The runtime additionally reconciles the
list against your handler table and will not activate if they disagree, so
listing an event you don't handle — or handling one you didn't list — is an
error rather than a silent no-op.
commands lists every command the module provides. Each id starts with the
module id and a dot, is lower-case and dotted, and appears once. Like
events, it is checked in both directions:
the runtime will not activate a module whose bound commands and
signed list disagree.
capabilities decides what your module can reach,
and it is enforced.At activation the host mints a capability table
from this list;
a group you did not declare is simply absent,
and the matching SDK call returns its failure value and logs once instead of
doing anything.Two kinds of name go in the list :
| declared | kind | effect | | -- -- -- -- -- -| -- -- -- --|
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --|
| `gpg` | granted | sign,
encrypt, decrypt, verify, keys, key lists,
result analysis | | `pgp` | granted | packet - structure inspection,
no keyring or engine | | `ui` | granted | a UI script, commands,
theme colours by role | | `ui.custom` | granted |
adds native widgets mounted by the script; requires `ui` |
| editor | granted | reading the document the user currently has open |
| storage | granted | application settings, the caches, the runtime register table |
| process | granted | running an external program |
| network | recorded | not enforced — see below |
Buffers, memory, logging, event subscription and translations are always available and are not declared.
network is recorded, signed and shown to the user, but the host does not
mediate it: your module opens a socket through Qt, and there is nothing in
between for the host to withhold. It is kept separate from the granted list
rather than mixed in, so that a permission list never claims to enforce
something it cannot.
Declaring a capability you do not use costs the user trust for nothing. Failing to declare one you do use is caught at run time, by a log line naming the capability and the entry point that wanted it — for example:
GFGpgDecrypt needs the "gpg" capability, which this module's signed manifest
does not declare. Add it to module.json's "capabilities" and rebuild; the
call did nothing.
An unknown capability name fails the build, at configure time, naming the vocabulary.
3. Implement the module
A module's UI is two parts: typed C++ commands, which do the work and carry every user-visible word, and a Lua UI script, which says where those commands are offered and when they are enabled.
// MyModule.cpp
#include <GFModule.h>
#include <GFSDKHostCommands.hpp> // the Host's own commands: AppMessage, ...
#include "GFModuleIdentity.h" // generated from module.json by gf_add_module()
#include "GFModuleTr.h" // GTrC: this module's translation context
struct ShowAbout {
static constexpr gf::cmd::Meta kMeta{
GF_MODULE_ID ".show_about", GTrC::Noop("About My Module"), "", "",
0, gf::cmd::kNeedsGuiThread};
using Args = gf::cmd::Unit;
using Result = gf::cmd::Unit;
};
auto DoShowAbout(const gf::cmd::CommandContext&, const gf::cmd::Unit&)
-> gf::cmd::Outcome<gf::cmd::Unit> {
Commands().Invoke<gf::cmd::host::AppMessage>(
{gf::cmd::host::AppMessage::Severity::kInfo,
GTrC::tr("About My Module"), GTrC::tr("Hello.")});
return gf::cmd::Outcome<gf::cmd::Unit>::Success({});
}
auto OnActivate() -> GFResult {
LOG_INFO("MyModule activating");
return GFResult::Ok();
}
const std::array<gf::cmd::Binding, 1> kCommands = {
gf::cmd::Bind<ShowAbout, &DoShowAbout>()};
const GFModuleHooks kHooks = {
sizeof(GFModuleHooks),
GF_MODULE_ID,
GF_MODULE_VERSION,
GF_MODULE_TRANSLATION_CONTEXT,
&OnActivate,
nullptr,
nullptr,
nullptr,
0, // events it handles
kCommands.data(),
kCommands.size(), // commands it provides
};
extern "C" GF_MODULE_EXPORT auto GFModuleGetApi(uint32_t abi)
-> const GFModuleApi* {
return GFModuleRuntimeGetApi(abi, &kHooks);
}
```lua-- ui / main.lua : where the command is offered.No user - visible text here.local about = commands.get("com.example.my_module.show_about") ui.action { id = "about", anchor = ui.anchor("main.menu.help"), command = about }
Every hook in `GFModuleHooks` is optional; pass `nullptr` for one you have
nothing to say about. The runtime registers the commands before
`on_activate` and loads the script after it. At deactivation the host
withdraws everything the module registered -- commands, widgets, script,
subscriptions, translations -- before `on_deactivate` runs, so nothing has to
be unregistered by hand. What `on_deactivate` is for is the module's OWN work:
stop the threads it started, the timers it runs, the network requests it has
in flight. A module that starts none of those passes `nullptr`. `GFModuleGetApi` is
the one piece of ABI a module writes by hand, deliberately not hidden behind
a macro: it is what makes the linker keep `gf_module_runtime`'s entry point,
since nothing else in a module references it. The host resolves the symbol
after the module is linked.
Check the real modules for complete examples:
[`m_pgp_inspect`](src/m_pgp_inspect) (a command and a dialog widget) and
[`m_key_server_sync`](src/m_key_server_sync) (commands with arguments, a
dialog, a settings page and key details buttons).
### 4. Add CMakeLists.txt
```cmake
gf_add_module(
NAME my_module
QT Core Widgets
LUA_SCRIPTS ui/main.lua # embedded, signed with the module, loaded by the host
#UI_DIR ui #if you have a Qt Designer forms directory
#LINK some_target #extra libraries, if any
)
LUA_SCRIPTS compiles the listed scripts into the module under
:/gf_module/<id>/lua/. They are data: the host runs them in its own sandbox,
and a module must never link an interpreter of its own
(scripts/check_module_boundary.sh refuses one).
gf_add_module() reads module.json, builds the target from every source
file in the directory (or SOURCES if given explicitly), generates the
identity header, wires up translations for every locale in
GPGFRONTEND_SUPPORTED_LOCALES, and builds + signs the resulting
.gfmodule package — all from those handful of values. See
modules/src/m_pgp_inspect/CMakeLists.txt
for a real, small example. RESOURCES adds Qt resource files and
INCLUDE_DIRS extra include directories.
After adding or changing GTrC::tr()/GTrC::Noop() strings, run
scripts/update_translations.sh to sync
every locale's .ts file — it creates any that are missing, so you never
hand-author them.
Match the Qt major version of the host build — a version mismatch will prevent the module from loading.
Key Concepts
What a module may link
Two static archives, both module-side, both linked for you by
gf_add_module():
gf_sdk— the public SDK. EveryGFSDK*function you call is defined here, forwarding to the capability table the host handed your module. It is stateless: no bound table, no current module, no thread-local.gf_module_runtime— the per-module runtime state: your context, identity, hooks, event dispatch, translations.
Your module has no link edge to gf_core, gf_ui or gf_host_api, and
cannot acquire one: gf_add_module() fails at configure time if a host
library appears anywhere in your link closure, the module is linked with
-Wl,--no-undefined, and scripts/check_module_boundary.sh inspects the
built native afterwards and fails if it imports any GF* symbol or exports
anything but GFModuleGetApi.
If you need something the SDK does not offer, the change is a primitive in
src/sdk/GFSDKHostApi.h, its implementation in src/sdk/host/, and a wrapper
in src/sdk/api/ — not a link line.
Every host call takes a context
auto OnSomething(const GFEvent& e) -> GFEventResult {
auto* ctx = e.Context(); // from the event
auto keys = gf::sdk::ExportKey(ctx, channel, key_id, true);
LOG_I() << "exported" << keys.size() << "bytes";
return GFEventResult::Ok();
}
Outside a handler, GFModuleSdkContext() returns the same context. It is a
plain load of a pointer that never changes after activation, so it is safe
from any thread — but work you start on a worker should capture the
context rather than look it up there. Capturing is what makes a piece of
asynchronous code say which module it belongs to, which is the whole reason
the context is an argument instead of a global.
Functions that need nothing from the host take no context at all:
GFCompareSoftwareVersion and GFUIHumanSize are the two.
Logging
Five levels, LOG_T (trace) through LOG_E (error). The stream form is the
one to reach for:
LOG_W() << "open" << path << "failed:" << file.errorString();
LOG_T() << "option line:" << line << "fields:" << fields.size();
It takes no format string,
so there is no placeholder to get wrong and no arity to mismatch,
and it prints anything QDebug can — enums, `bool`,
QByteArray, containers.It is spelled exactly like the host's own
LOG_W() << ...,
and — like every other module - side call — needs no explicit context
: it reads GFModuleSdkContext() internally.
The older forms remain :
```cpp LOG_INFO("started"); // one fixed string FLOG_INFO("started with %1 accounts", count); // Qt placeholders, NOT printf
`FLOG_*` uses `QString::arg`, so `%1`/`%2` — a printf `%d` prints itself, and
an argument `arg()` has no overload for must be converted by hand. That is
what the stream form avoids.
Each module logs under its own category, named for its id, so a log line says
which module wrote it and which of its source lines it came from:
[module.email] [W] EMailImapController.cpp:412 - imap connect failed
Trace is for per-item chatter and stays off at `--log-level debug`; run with
`--log-level trace` to see it.
### Memory management
**SDK arguments are borrowed.** Pass a string straight through —
`id.toUtf8().constData()` for a `QString`, or a string literal — and keep
owning it. Do not pre-allocate a copy to hand over; nothing on the other side
frees it.
**SDK return values are owned.** Reclaim a returned `char*` with `UDUP` (or
`USECDUP` for one from the secure/wiping allocator):
```cpp
QString locale = UDUP(GFAppLocale(GFModuleSdkContext()));
The one exception to "arguments are borrowed" is a struct a module builds and
hands over whole — GFModuleEvent, GFModuleEventParam,
GFCommandExecuteContext and the like — whose char* members still have to
be allocated with GFMemStrDup (DUP(...)), because ownership of the
whole struct is what transfers. UDUP takes a char* the SDK handed you;
it does not accept a QByteArray or QString --those are already owned,
and freeing their storage through the SDK corrupts the heap,
so the call does not compile.
```cpp DUP("hello") // char owned by the callee — only for a transferred // struct field SECDUP("secret") // same, from the wiping allocator, for a secret UDUP(ptr) // consume an owned char return value, get a QString USECDUP(ptr) // same as UDUP, but frees with the wiping allocator
## #Events
Bind event ids to handlers in a static `GFEventBinding[]` table(
see step 3 above) — there is no separate subscribe call,
and the set must match
`module.json`'s `events` array exactly. Every handler returns a
`GFEventResult`; the runtime does the transport, so there is nothing to
answer manually on the common path.
Which events exist, and what each one MEANS, is written down once in
[`src/core/module/ModuleEventRegistry.cpp`](../src/core/module/ModuleEventRegistry.cpp).
That catalogue is what the host checks a subscription against, so an id that
is not in it is refused rather than silently never delivered. Each entry
records:
- **observe or extend.** Most events tell you something happened and do not
read your reply. A small, deliberate set are extension points where what
you return changes the outcome: the six `EDIT_TAB_TYPE_<TYPE>_OP_*` crypto
operations (your reply replaces the user's document) and the key-server
requests.
- **whether the reply is read at all.** `TAB_ACTIVATED` tells you a fact and
ignores what you return.
- **whether you may defer.** Where the flag allows it, return
`GFEventResult::Deferred()` and answer later through `event.Answer()`,
from any thread. The host accepts one answer per delivered event. If your
module is deactivated before it answers, the host answers for it, with a
failure, and refuses the late answer.
Nothing is a veto. No trigger site cancels an operation on a module's word,
and `TriggerEvent` is asynchronous and returns nothing, so a veto is not
expressible today.
No event carries a Host object. Everything an event hands you is data, and
UI comes from your script and your own widgets (see [UI](#ui)).
A handler that starts asynchronous work returns `GFEventResult::Deferred()`
and answers later through `event.Answer()`, from any thread, at most once.
See the key-server requests in
[`m_key_server_sync/KeyServerSyncModule.cpp`](src/m_key_server_sync/KeyServerSyncModule.cpp)
for a real example.
### Runtime values
A shared, typed register table (`storage` capability) for live
configuration, visible to the host and to other modules. A module may read any
namespace but WRITES only its own, which is its module id (`GFModuleId()`).
Keys and namespaces are lower-case; text and bool are kept apart because a lookup of the wrong
type is a miss, not a conversion. Use the `gf::sdk::` wrappers
(`GFSDK.hpp`), which apply a fallback where the raw calls only report
absence:
```cpp
auto* ctx = GFModuleSdkContext();
gf::sdk::SetStateBool(ctx, GFModuleId(), "ready", true);
bool ready = gf::sdk::StateBool(ctx, GFModuleId(), "ready", /*fallback=*/false);
gf::sdk::SetStateText(ctx, GFModuleId(), "last_error", message);
QString v = gf::sdk::StateText(ctx, GFModuleId(), "last_error");
The host publishes into core by itself: which engines are usable, every
module's lifecycle state, every registered command, and usage counters for
events, commands and OpenPGP operations. Those counters and flags are
integers, which a module reads (never writes) with gf::sdk::StateInt:
bool gnupg = gf::sdk::StateBool(ctx, "core", "engine.gnupg.supported");
qint64 runs = gf::sdk::StateInt(ctx, "core", "stats.operations.op_encrypt.run");
The full key list is in the SDK README.
Cache
Three tiers, one set of calls, differing in how long a value lives and
whether it is wiped — GF_STORE_SESSION (in memory), GF_STORE_DURABLE
(disk, plain), GF_STORE_SECURE_DURABLE (disk, wiping allocator):
auto* ctx = GFModuleSdkContext();
gf::sdk::SetCacheText(ctx, GF_STORE_SESSION, "key", "value");
gf::sdk::SetCacheText(ctx, GF_STORE_DURABLE, "token", "abc", /*ttl_seconds=*/3600);
QString v = gf::sdk::CacheText(ctx, GF_STORE_DURABLE, "token");
gf::sdk::RemoveCache(ctx, GF_STORE_DURABLE, "token");
GPG operations
Every host call takes the context explicitly, first argument, per
Every host call takes a context. Crypto
entry points return through a move-only GFGpgResult(ctx), which owns
whatever native result it wraps and releases it on every path (including an
early return) through its own destructor. Payloads cross the boundary as
GFBuf/GFBufferView, not raw char*, since a signature or an encrypted
blob is exact octets rather than text.
auto* ctx = GFModuleSdkContext();
auto in = GFBuf::Copy(ctx, plain_bytes);
QVector<const char*> key_ids = ...; // one entry per recipient/signer key id
GFGpgResult r(ctx);
if (GFGpgEncrypt(ctx, channel, key_ids.data(), key_ids.size(), in.View(),
/*ascii=*/1, r.Out()) != GF_GPG_OK) {
// r.ErrorString() explains why the call itself failed
}
if (r.Error() != GF_GPG_ERR_NO_ERROR) {
// the operation ran but gpg reported an error; r.ErrorString() again
}
auto encrypted = r.DataCopy();
GFGpgSign, GFGpgDecrypt and GFGpgVerify follow the same shape. Use
GFGpgCurrentChannel(ctx) to obtain the active channel from the main window.
See
m_email/EMailBasicGpgOpera.cpp for
real, complete call sites including error handling.
If you need the same structured result the host's own crypto dialogs show
(recipients, signatures, validity), use gf::sdk::AnalyseResult(ctx,
channel, operation, err, r.CapsuleId()) — this is engine neutral, working
the same way whether the active engine is GnuPG or rPGP.
UI
The full reference (the Lua API, the anchor catalog, the sandbox, the typed
widget protocols) is in src/sdk/README.md.
In short:
- Commands (
ui) are the semantic layer. A command has a title, a description and a category, allGTrC::Noopstrings, and typed arguments. Invoke one withCommands().Invoke<C>(args), the host's included (gf::cmd::host, inGFSDKHostCommands.hpp). - The UI script (
ui) places commands on anchors withui.action, and reacts to a small, closed set of UI events withui.subscribe. - Native widgets (
ui.custom) are your ownQWidgets. Derive from one ofgf::ui::DocumentWidget,SettingsWidgetorDialogWidget, register it inOnActivate(), and mount it from the script withui.mount. The host owns the frame around it: the dialog, the settings page, the document tab.
auto OnActivate() -> GFResult {
gf::ui::RegisterNativeWidget<MySettingsPage>(
"settings", {GTrC::Noop("My Module"), GTrC::Noop("proxy,timeout")},
[](const QCborMap&) { return new MySettingsPage(); });
return GFResult::Ok();
}
ui.mount {
id = "settings", anchor = ui.anchor.settings{section = "features"},
widget = native.widget("settings") }
Register titles and keywords untranslated, with GTrC::Noop(...): the host
translates them when it shows them, in your module's context, so they follow a
language change.
Settings sections are application, keys_engines, features and system.
Migrating from the object-based UI API
Every entry point that handed a module a Host object, or took one, now refuses (and logs the replacement once). One example per pattern:
A menu entry. Before: handle MAINWINDOW_MENU_MOUNTED, RequireGui the
menu, addAction. After: a command, placed by the script.
ui.action {
id = "check", anchor = ui.anchor("main.menu.help"),
command = commands.get("com.example.my_module.check") }
A key details button. Before: handle KEY_PAIR_OPERA_MENU_CREATED. After:
the key.details.actions anchor; the host groups buttons by command category
and passes the key.
ui.action {
id = "publish", anchor = ui.anchor("key.details.actions"), command = publish,
update =
function(ctx) if not ctx.key then return {visible = false} end return {
args = { key = ctx.key }
}
end }
An action on selected keys. The key.list.context anchor adds an entry
to the key lists' context menus. ctx.keys holds the keys the Host's own key
actions would act on: the checked keys when any are checked, otherwise the
selected ones. ctx.key is set too when there is exactly one. The command declares
std::optional<QList<gf::cmd::KeyRef>> keys.
ui.action {
id = "refresh_selected", anchor = ui.anchor("key.list.context"),
command = refresh,
update =
function(ctx) if #ctx.keys == 0 then return {visible = false} end return {
args = { keys = ctx.keys }
}
end }
A shortcut. An action on a main.menu.* anchor may set shortcut =
"Ctrl+M". A key sequence the Host or an earlier module already uses is
dropped, with a warning in the log.
A dialog with a parent. Before: GUI_OBJECT, GFUIShowDialog(ctx, dlg,
parent). After: a DialogWidget, a dialog mount, and the host command
org.gpgfrontend.view.open, which accepts only your own mounts.
local inspector = ui.mount {
id = "inspector", anchor = ui.anchor.dialog{},
widget = native.widget("inspector") }
A settings page, or widgets injected into the Network tab. Before:
RegisterSettingsPage with SetSettings/ApplySettings found by name, or the
NETWORK_SETTINGS_TAB_* events. After: a SettingsWidget
(LoadSettings/ApplySettings) and a settings mount, as above.
Reading and writing settings. Before: GFStorageSettingsRoot and a cast
to QSettings*. After: the module's own group, no cast.
auto* ctx = GFModuleSdkContext();
bool check = gf::sdk::Setting(ctx, GF_SETTING_MODULE, "check_on_startup", true)
.toBool();
gf::sdk::SetSetting(ctx, GF_SETTING_MODULE, "check_on_startup", false);
In Lua, state.get / state.set read and write the same group.
Opening a document in a new tab. Before: invokeMethod(edit,
"SlotNewCustomTab") and setTabText on the host's tab widget. After:
Commands().Invoke<gf::cmd::host::DocumentOpen> with the content as a gf::cmd::Blob.
A document type of your own. Before: RegisterTabPageView,
register_file_extension with its FILE_EXT_* events, and an
EDIT_TAB_TYPE_*_OP_SAVE_FILE handler. After: a DocumentWidget
registered with RegisterNativeWidgetFactory, mounted on
ui.anchor.editor {
document_type = ..., extensions = {... } }. The host
opens those files, and saves through your Save and PrepareSave.
A view with chrome of its own (headers, attachments) can add
compact_status = true to that anchor: while one of its tabs is current,
the Status Panel starts as a thin bar and opens again on a new result.
Showing the raw source. Before: AdoptSourceView(QWidget*) took the
host's editor. After: the page's own switcher shows the source; ask for it
with ShowSource(true), and keep it read-only with SourceLockReason.
More of the module-facing API
Things the sections above do not show, each documented in its header:
- Commands (
GFSDKCommand.hpp,GFModuleCommand.h). A handler may take agf::cmd::Reply<Result>as a third argument and answer later, from any thread. A command type may definestatic auto State(const CommandContext&) -> uint32_tto say whether it is enabled, visible or checked;kCheckablein itskMetaflags makes it a toggle. A long handler pollsctx.Cancelled().Commands()also offersInvokeDynamic(by id, CBOR arguments),Cancel,DescribeandList. - Events (
GFModuleEvent.h).event.Bytes(key)for a binary parameter (no base64 round trip),event.Require(key, out)for a mandatory one,event.Params()for all of them;event.Answer()gives the handle a deferred handler answers through (Ok,Fail,Send,Answered). - Facts (
GFModule.h).GFModuleId(),GFModuleVersion(),GFModuleHasCapability(name),GFModuleIsVerified(). - Logging (
GFModuleLog.h).MLogTracethroughMLogErrortake one QString;LOG_*take a format string andFLOG_*a format string with%1-style arguments.
Translations
Include the generated GFModuleTr.h and wrap strings with GTrC::tr("..."),
or with GTrC::Noop("...") for a string that must stay untranslated until
later, such as a command title or a native widget's title. GTrC is declared
by gf_add_module() in your module's own translation context, the
translation_context from module.json, so every module's strings live in a
context of their own and never collide with another module's. The host
translates GTrC::Noop text in the context of the module that registered it.
Always spell it GTrC::tr / GTrC::Noop. lupdate does not expand macros or
see through helpers: a local Tr() wrapper, a #define, or
QCoreApplication::translate with a context other than the literal
translation_context is silently left out of the .ts files.
There is nothing left to register: gf_module_runtime hands the host the
module's translator during activation, before any event subscription and
before OnActivate() runs, and the host installs it on the GUI thread. It is
in place by the time anything the module mounts is shown -- but not
necessarily while OnActivate() itself runs, so mark text there with
GTrC::Noop and let the host translate later.
.ts files go in ts/<translation_context>.<locale>.ts and are embedded as
Qt resources under :/i18n/. The locale set is not chosen per module — it
comes from the shared GPGFRONTEND_SUPPORTED_LOCALES list (see
Build Notes), so the module always offers the same languages
as the application. After adding or changing translatable strings, run
scripts/update_translations.sh to sync
every locale's .ts.
Packaging, Signing & Distribution
Every module ships as a signed .gfmodule package (a manifest and its Ed25519
signature, plus, for an external module, the publisher public key that
signature was made with) and the native shared library it names. The package
format, verification rules, and trust model live in
src/core/module/; this section only summarises the
parts that matter for building and distributing a module.
There are three tools, one per trust role:
| Tool | Role |
|---|---|
gf_module_keygen |
the integrated build identity: one ephemeral key per build tree |
gf_module_packager |
builds, reseals and verifies integrated packages |
gf_module_externalize |
turns a verified integrated package into a publisher-signed external one |
Integrated modules (the ones in this directory)
Nothing to do by hand: gf_add_module() builds, signs, and installs the
package for you as part of the normal build, using an Ed25519 keypair
generated once per build tree (gf_module_keygen, invoked automatically by
the top-level CMake configure). That key is compiled into the host binary as
its trust root and is never exported, published, or reused across builds —
it exists only to prove a package was produced by this build tree, not to
identify a publisher.
The resulting layout, under the modules root (build/artifacts/modules in a
dev build):
<namespace-root>/<key>/module.gfmodule # manifest + signature
<namespace-root>/<key>/native/libgf_mod_xxx.so
<key> is derived from the module's id (ModuleDirectoryKey(), e.g.
com.bktus.gpgfrontend.module.email → email-a1b2c3d4e5f60718293a); it is a
locator only, never trusted on its own — the manager re-derives it from the
signed manifest's id and refuses a descriptor sitting in the wrong
directory.
External modules (a user's own)
An external module lives outside the application entirely, in the user's
profile under mods/<key>/, with the same module.gfmodule + native/
layout as above. For it to load:
- The user sets Settings → General → Module Discovery to "Also Look For
Modules I've Added". This only controls whether the
mods/directory is scanned at all — finding a module there never implies running it. - In the Module Controller, the user trusts the publisher key the module was signed with ("signatures by this key are worth considering") and then enables that specific module ("run this one"). These are deliberately two separate decisions: trusting a key applies to every module that key ever signs, so collapsing it with "run this one" into a single click would make the first of them too easy to grant by accident.
- Both take effect on the next restart.
The publisher key inside the package only names the signer. It never grants trust by itself: trust comes only from the user's decision about that exact key. A publisher name or URL in the manifest is an unverified claim. The build's own integrated key is refused wherever a publisher key is expected.
Publishing an external module
External modules are made from integrated ones, never built directly. The integrated package is the proof of what was built; externalizing it is a trust-domain transition from "this build signed it" to "this publisher signs it", done once, as the last release step.
- Create a publisher key, once. It is your lasting identity, so keep the
secret file safe and publish the
.pubfile (or its fingerprint) through a channel your users trust:
bash
gf_module_externalize new-key --out publisher.key # writes publisher.key (0600) + publisher.key.pub
gf_module_externalize fingerprint --key publisher.key.pub
The key files are typed text files. A raw 32-byte seed, such as a build
tree's module-build.seed, is refused, so the build key can never be used
as a publisher key by mistake.
- Build with native binding required, so the integrated descriptor binds
the exact bytes of its native. Finish every step that rewrites the native
(strip,
patchelf, deploy tools) and the integrated reseal first:
bash
cmake -B build -DGPGFRONTEND_INTEGRATED_MODULE_NATIVE_BINDING=REQUIRED
- Externalize the module's namespace directory:
bash
gf_module_externalize \
--input build/artifacts/modules/<key> \
--publisher-key publisher.key \
--output-root dist \
[--publisher-name "Example"] [--publisher-url https://example.com]
The input is first verified as an integrated module of this build, and
refused if its native is unbound or its native/ directory holds anything
besides the entry native. The output is dist/<key>/module.gfmodule plus
dist/<key>/native/<entry>, ready to drop into a user's mods/. Every
manifest field is carried over unchanged except the optional publisher
metadata; the tool refuses a manifest field it has no rule for. It never
overwrites an existing output.
- Do not modify the output. The descriptor binds the native's bytes, and there is no external reseal: that would need your secret key wherever the rewrite happened. The one exception is Authenticode-signing a Windows native, whose binding digest excludes the signature by design.
gf_module_packager verify-module-set checks integrated trees only, and
refuses an externalized namespace.
macOS has no external modules
External modules are refused unconditionally on macOS: Library Validation
means the process cannot load a dylib that does not carry this
application's own Team ID, and a third-party module by definition does not.
This is a platform policy, not a missing feature — admitting one past
GpgFrontend's own checks would still have it refused by dyld.
Licensing
All modules in this directory are released under GPL-3.0-or-later, consistent with the main GpgFrontend project.
