# SPDX-License-Identifier: CC0-1.0
# pyright: reportMissingImports=false, reportUndefinedVariable=false
# flake8: noqa
"""
Utility build hook to generate and fix a compile_commands.json suitable for
clangd. Prepares the compilation database and propagates -I flags to library
entries under `lib/`.
Additionally, in environments with multiple toolchains installed, this script restricts
include paths to a single specified toolchain to ensure deterministic include
resolution.
Usage (must be run as a pair):
pio run -t compiledb && pio run -t fix_compiledb
Run `compiledb` first, then `fix_compiledb`.
Version Info (pio --version)
PlatformIO Core, version 6.1.18
"""
import glob
import json
import os
import sys
import shlex
Import("env") # type: ignore
def pre_compiledb_actions(toolchain, framework_libs=None):
"""
Prepare compile_commands.json for clangd:
- restrict CPPPATH to the specified toolchain and Arduino framework libs
- set COMPILATIONDB_PATH to the build directory
framework_libs_subdirs: optional list of sub-path patterns under PROJECT_PACKAGES_DIR
e.g. [os.path.join("arduinoespressif32", "libraries")]
"""
print("start pre_compiledb_actions")
# turn off default toolchain includes
env.Replace(COMPILATIONDB_INCLUDE_TOOLCHAIN=False)
# restrict CPPPATH to xtensa-esp32s3-elf include paths to avoid mixing toolchains.
toolchain_paths = [
p for p in env.DumpIntegrationIncludes().get("toolchain", []) if toolchain in p
]
# add path(s) to Arduino framework libraries (support multiple framework dirs)
packages_dir = env.get("PROJECT_PACKAGES_DIR")
framework_libs = framework_libs or []
framework_libs_dirs = []
for sub in framework_libs:
# join packages_dir with the provided sub-path pattern and glob
pattern = os.path.join(packages_dir, sub)
framework_libs_dirs.extend(glob.glob(pattern))
src_paths = []
for libraries_dir in framework_libs_dirs:
src_paths.extend(glob.glob(os.path.join(libraries_dir, "*", "src")))
libs_path = list(set(src_paths))
# replace CPPPATH for compiledb only
env.Replace(CPPPATH=toolchain_paths + libs_path)
# create compile_commands.json
env.Replace(COMPILATIONDB_PATH=os.path.join("$BUILD_DIR", "compile_commands.json"))
print("completed pre_compiledb_actions")
def fix_compiledb_action(*args, **kwargs):
"""
Post-process compile_commands.json:
- find the entry for the given project-relative source (args[1])
- extract its -I include flags and append missing ones to entries under 'lib/'
- write the file back only if modifications were made
"""
print("start fix_compiledb_action")
compile_commands_dir = os.path.normpath(args[0])
src_main = os.path.normpath(args[1])
compile_commands_path = os.path.join(compile_commands_dir, "compile_commands.json")
if not os.path.isfile(compile_commands_path):
print(
f"fix_compiledb_action: compile_commands.json not found at {compile_commands_path}",
file=sys.stderr,
)
return
compile_commands = []
with open(compile_commands_path, "r", encoding="utf-8") as fp:
compile_commands = json.load(fp)
match = next(
(
entry
for entry in compile_commands
if os.path.normpath(entry.get("file") or "") == src_main
),
None,
)
if not match:
print(f"fix_compiledb_action: no match {src_main}", file=sys.stderr)
return
cmd = match.get("command") or " ".join(match.get("arguments", []))
tokens = shlex.split(cmd)
includes = [t for t in tokens if t.startswith("-I")]
# propagate -I flags to entries under 'lib/'
lib_count = 0
if includes:
for entry in compile_commands:
rel = os.path.normpath(entry.get("file")).replace(os.sep, "/")
if rel.startswith("lib/"):
original_cmd = entry.get("command") or ""
existing_tokens = shlex.split(original_cmd)
# only append tokens that are not already present to avoid duplication
to_append = [inc for inc in includes if inc not in existing_tokens]
if to_append:
entry["command"] = original_cmd + (" " + " ".join(to_append))
lib_count += 1
if lib_count:
with open(compile_commands_path, "w", encoding="utf-8") as fp:
json.dump(compile_commands, fp, indent=2)
print(
f"Appended includes to {lib_count} lib entries in {compile_commands_path}"
)
print("completed fix_compiledb_action")
# pio run -t compiledb
if "compiledb" in COMMAND_LINE_TARGETS:
# Running `pio run -t compiledb` alone can aggregate include paths from all
# installed toolchains; to avoid that we explicitly restrict to a single
# toolchain here and collect includes only for it.
pre_compiledb_actions(
# ls -laF ~/.platformio/packages | grep toolchain
# drwx------ 6 hiromasa hiromasa 4096 10月 13 2024 toolchain-esp32ulp/
# drwx------ 7 hiromasa hiromasa 4096 9月 16 2024 toolchain-riscv32-esp/
# drwx------ 7 hiromasa hiromasa 4096 9月 16 2024 toolchain-xtensa-esp32s3/
# specify exactly one toolchain identifier, e.g. "xtensa-esp32s3-elf"
"xtensa-esp32s3-elf",
# ls -laF ~/.platformio/packages | grep frame
# drwx------ 6 hiromasa hiromasa 4096 10月 14 2023 framework-arduino-gd32v/
# drwx------ 6 hiromasa hiromasa 4096 9月 16 2024 framework-arduinoespressif32/
# drwx------ 8 hiromasa hiromasa 4096 10月 13 2024 framework-espidf/
# framework library subdirs under PROJECT_PACKAGES_DIR (joined with packages_dir inside function)
[
os.path.join("framework-arduinoespressif32", "libraries"),
],
)
# pio run -t fix_compiledb
env.AddCustomTarget(
"fix_compiledb",
None,
fix_compiledb_action(
# build directory where compile_commands.json is written (COMPILATIONDB_PATH)
# .pio/build/esp32-s3-devkitc-1
os.path.join(env.get("PROJECT_BUILD_DIR"), env.get("PIOENV")),
# source file whose compile_commands entry is used to extract -I include flags
# src/main.cpp
os.path.join("src", "main.cpp"),
),
)
When preparing your Windows machine, you have to decide between either using the MSVC toolchain or the GNU toolchain. If in doubt, go for MSVC since that is the default on Windows. You will want to go for the GNU toolchain if you depend on libraries that can only be compiled with the GNU toolchain.
とりあえず定番の ToDo リストハローワールド
Cargo.toml
[package]
name = "relm-test"
version = "0.1.0"
edition = "2024"
[dependencies]
relm4 = "0.9.1"
relm4-components = "0.9.1"
main.rs
use gtk::glib::clone;
use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, WidgetExt, EntryExt, EditableExt};
use relm4::{gtk, ComponentParts, ComponentSender, RelmApp, RelmWidgetExt, SimpleComponent};
struct AppModel {
tasks: Vec<String>,
}
#[derive(Debug)]
enum AppMsg {
AddTask(String),
RemoveTask(usize),
}
struct AppWidgets {
task_list: gtk::ListBox,
}
impl SimpleComponent for AppModel {
type Input = AppMsg;
type Output = ();
type Init = ();
type Root = gtk::Window;
type Widgets = AppWidgets;
fn init_root() -> Self::Root {
gtk::Window::builder()
.title("ToDo List")
.default_width(400)
.default_height(300)
.resizable(false)
.build()
}
fn init(
_init: Self::Init,
window: Self::Root,
sender: ComponentSender<Self>,
) -> relm4::ComponentParts<Self> {
let model = AppModel { tasks: Vec::new() };
let vbox = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(5)
.build();
let task_entry = gtk::Entry::builder()
.placeholder_text("Enter a new task")
.build();
let add_button = gtk::Button::with_label("Add Task");
let task_list = gtk::ListBox::new();
let scrolled_window = gtk::ScrolledWindow::builder()
.vexpand(true)
.child(&task_list)
.build();
window.set_child(Some(&vbox));
vbox.set_margin_all(5);
vbox.append(&task_entry);
vbox.append(&add_button);
vbox.append(&scrolled_window);
let add_task = {
let sender = sender.clone();
let task_entry = task_entry.clone();
move || {
if let Some(text) = task_entry.text().as_str().to_owned().into() {
if !text.is_empty() {
sender.input(AppMsg::AddTask(text));
task_entry.set_text("");
}
}
}
};
task_entry.connect_activate({
let add_task = add_task.clone();
move |_| {
add_task();
}
});
add_button.connect_clicked({
let add_task = add_task.clone();
move |_| {
add_task();
}
});
let widgets = AppWidgets { task_list };
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
match message {
AppMsg::AddTask(task) => {
self.tasks.push(task);
}
AppMsg::RemoveTask(index) => {
if index < self.tasks.len() {
self.tasks.remove(index);
}
}
}
}
fn update_view(&self, widgets: &mut Self::Widgets, sender: ComponentSender<Self>) {
while let Some(child) = widgets.task_list.first_child() {
widgets.task_list.remove(&child);
}
for (index, task) in self.tasks.iter().enumerate() {
let row = gtk::Box::new(gtk::Orientation::Horizontal, 5);
let label = gtk::Label::new(Some(task));
label.set_halign(gtk::Align::Start);
label.set_wrap(true);
label.set_wrap_mode(gtk::pango::WrapMode::Char);
label.set_xalign(0.0);
label.set_size_request(300, -1);
let remove_button = gtk::Button::with_label("Remove");
remove_button.set_halign(gtk::Align::End);
remove_button.connect_clicked(clone!(
#[strong]
sender,
move |_| {
sender.input(AppMsg::RemoveTask(index));
}
));
row.append(&label);
row.append(&remove_button);
widgets.task_list.append(&row);
}
widgets.task_list.show();
}
}
fn main() {
let app = RelmApp::new("relm4.test.todo_list");
app.run::<AppModel>(());
}
ToDo:
WIndows でも試すこと。もう少しです。こんにちは。
Ubuntu で動作させるとテキストボックスに日本語が入らない確認。
ダークモードメモ。
fn main() {
// Initialize GTK
gtk::init().expect("Failed to initialize GTK");
// Enable dark mode
if let Some(settings) = gtk::Settings::default() {
settings.set_gtk_application_prefer_dark_theme(true);
}
let app = RelmApp::new("relm4.test.todo_list");
app.run::<AppModel>(());
}