diff mbox series

[v2,03/18] devtool: ide-sdk: export recipe Cargo configuration

Message ID 20261109-devtool-ide-sdk-cargo-v2-3-d56134000561@savoirfairelinux.com
State New
Headers show
Series devtool: ide-sdk: add support for Rust recipes | expand

Commit Message

Enguerrand de Ribaucourt Nov. 9, 2026, 1:44 p.m. UTC
Export recipe Cargo config to the devtool workspace so Cargo and
rust-analyzer reuse target, linker, rustflags, and dependency settings
instead of duplicating them in settings.json and tasks.json. Other IDE
integrations can reuse the generated config too.

The original input is generated by cargo_common.bbclass. Updating it
in-place would require a Python TOML writer dependency, so regenerate it
with a bounded writer helper.

AI-Generated: Uses GitHub Copilot
Signed-off-by: Enguerrand de Ribaucourt <enguerrand.de-ribaucourt@savoirfairelinux.com>

---
Changes in v2:
- Replace in-place Cargo config updates with a helper that rewrites
  the full file.
- Generate Cargo config for all IDE modes.
---
 scripts/lib/devtool/ide_sdk.py | 102 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 102 insertions(+)
diff mbox series

Patch

diff --git a/scripts/lib/devtool/ide_sdk.py b/scripts/lib/devtool/ide_sdk.py
index e686dbcff8..98e39703e7 100755
--- a/scripts/lib/devtool/ide_sdk.py
+++ b/scripts/lib/devtool/ide_sdk.py
@@ -22,6 +22,7 @@  from pathlib import Path
 
 import scriptutils
 import bb
+from bb._vendor import tomli
 from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, DevtoolError, parse_recipe
 from devtool.standard import get_real_srctree
 from devtool.deploy import parse_packages_arg
@@ -787,6 +788,41 @@  class ExecutableBinary:
         return None
 
 
+def _toml_dumps(config):
+    """Serialize TOML values and tables."""
+    def format_value(value):
+        if isinstance(value, str):
+            return json.dumps(value)
+        if isinstance(value, bool):
+            return str(value).lower()
+        if isinstance(value, (int, float)):
+            return repr(value)
+        if isinstance(value, list):
+            return '[%s]' % ', '.join(format_value(item) for item in value)
+        if isinstance(value, dict):
+            return '{ %s }' % ', '.join(
+                '%s = %s' % (json.dumps(key), format_value(item))
+                for key, item in value.items())
+        raise TypeError('Unsupported TOML value: %s' % type(value).__name__)
+
+    lines = []
+
+    def write_table(table, path=()):
+        if path:
+            if lines:
+                lines.append('')
+            lines.append('[%s]' % '.'.join(json.dumps(key) for key in path))
+        for key, value in table.items():
+            if not isinstance(value, dict):
+                lines.append('%s = %s' % (json.dumps(key), format_value(value)))
+        for key, value in table.items():
+            if isinstance(value, dict):
+                write_table(value, path + (key,))
+
+    write_table(config)
+    return '\n'.join(lines) + ('\n' if lines else '')
+
+
 class RecipeModified:
     """Handling of recipes in the workspace created by devtool modify"""
     OE_INIT_BUILD_ENV = 'oe-init-build-env'
@@ -1122,6 +1158,70 @@  class RecipeModified:
         self.recipe_id = self.bpn + "-" + self.package_arch
         self.recipe_id_pretty = self.bpn + ": " + self.package_arch
 
+    @property
+    def cargo_config_path(self):
+        return os.path.join(self.ide_sdk_dir, 'cargo-config.toml')
+
+    def update_cargo_config(self):
+        """Export the recipe Cargo config to the devtool workspace."""
+        source_config = os.path.join(self.cargo_home, 'config.toml')
+        try:
+            with open(source_config, 'r') as config_file:
+                cargo_config = tomli.loads(config_file.read())
+        except FileNotFoundError as exc:
+            raise DevtoolError(
+                'Cargo config not found at %s; build the recipe first' % source_config) from exc
+
+        if 'build' not in cargo_config:
+            cargo_config['build'] = {}
+        cargo_config['build']['target'] = self.cargo_target
+
+        if 'target' not in cargo_config:
+            cargo_config['target'] = {}
+        target_config = cargo_config['target']
+        if self.cargo_target not in target_config:
+            target_config[self.cargo_target] = {}
+        target_config = target_config[self.cargo_target]
+        target_config['linker'] = self.cargo_linker
+        target_config['rustflags'] = shlex.split(self.cargo_rustflags)
+
+        if 'env' not in cargo_config:
+            cargo_config['env'] = {}
+        env_config = cargo_config['env']
+        env_names = (
+            'CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'AR',
+            'TARGET_CC', 'TARGET_CXX', 'TARGET_CFLAGS', 'TARGET_CXXFLAGS', 'TARGET_AR',
+            'HOST_CC', 'HOST_CXX', 'HOST_CFLAGS', 'HOST_CXXFLAGS', 'HOST_AR',
+            'RUST_TARGET_PATH', 'RUST_BACKTRACE', 'SOURCE_DATE_EPOCH',
+            'PKG_CONFIG', 'PKG_CONFIG_FOR_BUILD', 'PKG_CONFIG_FOR_TARGET',
+            'PKG_CONFIG_SYSROOT_DIR', 'PKG_CONFIG_PATH', 'PKG_CONFIG_LIBDIR',
+            'LIBGIT2_NO_VENDOR', 'LIBSSH2_SYS_USE_PKG_CONFIG',
+            'LIBSQLITE3_SYS_USE_PKG_CONFIG', 'OPENSSL_NO_VENDOR',
+            'PKG_CONFIG_ALLOW_CROSS', 'PKG_CONFIG_ALL_DYNAMIC',
+            'SYSTEM_DEPS_BUILD_INTERNAL', 'ZSTD_SYS_USE_PKG_CONFIG',
+        )
+        for name in env_names:
+            value = self.cargo_env.get(name)
+            if value is not None:
+                env_config[name] = {'value': str(value), 'force': True}
+
+        content = (
+            '# Generated by devtool ide-sdk for the active build environment.\n'
+            '\n'
+            + _toml_dumps(cargo_config))
+
+        config_path = self.cargo_config_path
+        os.makedirs(os.path.dirname(config_path), exist_ok=True)
+        try:
+            with open(config_path, 'r') as config_file:
+                current = config_file.read()
+        except FileNotFoundError:
+            current = None
+        if current != content:
+            with open(config_path, 'w') as config_file:
+                config_file.write(content)
+        return config_path
+
     @staticmethod
     def is_valid_shell_variable(var):
         """Skip strange shell variables like systemd
@@ -2019,6 +2119,8 @@  def ide_setup(args, config, basepath, workspace):
                 image.extract_nfs_rootfs(nfs_export_base_dir, args.nfs, args.target)
     if DevtoolIdeMode.modified in modes:
         for recipe_modified in recipes_modified:
+            if recipe_modified.build_tool is BuildTool.CARGO:
+                recipe_modified.update_cargo_config()
             if recipe_modified.build_tool is BuildTool.CMAKE:
                 recipe_modified.cmake_preset()
             if recipe_modified.build_tool is BuildTool.MESON: