From d231bf468a2ab84a28072b91aaa6ae26283556be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:35:47 +0000 Subject: [PATCH 1/7] Initial plan From 10d5ce69bb6c511c57d4ce4666cae072b6ec6661 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:40:54 +0000 Subject: [PATCH 2/7] Add configuration file support to dmd, dmr, and dmf-get Co-authored-by: JohnAmadis <17320783+JohnAmadis@users.noreply.github.com> --- .../lib/dmod_dependencies/dmod_dependencies.c | 94 +++++++++++--- .../lib/dmod_dependencies/dmod_dependencies.h | 6 + tools/system/dmf-get/main.c | 119 ++++++++++++++++++ 3 files changed, 204 insertions(+), 15 deletions(-) diff --git a/tools/lib/dmod_dependencies/dmod_dependencies.c b/tools/lib/dmod_dependencies/dmod_dependencies.c index d3b83878..ebe0bbad 100644 --- a/tools/lib/dmod_dependencies/dmod_dependencies.c +++ b/tools/lib/dmod_dependencies/dmod_dependencies.c @@ -60,7 +60,7 @@ static void SetError(Dmod_DependenciesContext_t* ctx, const char* format, ...) { * @brief Add a dependency entry to the list */ static bool AddEntry(Dmod_DependenciesContext_t* ctx, const char* name, - const char* version, const char* manifest) { + const char* version, const char* manifest, const char* config) { DependencyNode_t* node = (DependencyNode_t*)Dmod_Malloc(sizeof(DependencyNode_t)); if (!node) { SetError(ctx, "Out of memory"); @@ -86,6 +86,12 @@ static bool AddEntry(Dmod_DependenciesContext_t* ctx, const char* name, strncpy(node->entry.manifest, manifest, DMOD_DEPENDENCIES_MAX_URL_LEN - 1); node->entry.manifest[DMOD_DEPENDENCIES_MAX_URL_LEN - 1] = '\0'; + // Copy config if provided + if (config) { + strncpy(node->entry.config, config, DMOD_DEPENDENCIES_MAX_CONFIG_LEN - 1); + node->entry.config[DMOD_DEPENDENCIES_MAX_CONFIG_LEN - 1] = '\0'; + } + // Add to list if (ctx->tail) { ctx->tail->next = node; @@ -187,7 +193,7 @@ static bool ParseLine(Dmod_DependenciesContext_t* ctx, char* line) { return true; } - // Parse module entry: module[@version] [<$from manifest_url>] + // Parse module entry: module[@version] [config_path] [$from manifest_url] // First check if there's an inline $from directive char* from_pos = strstr(line, "$from"); char* manifest_for_entry = NULL; @@ -225,9 +231,12 @@ static bool ParseLine(Dmod_DependenciesContext_t* ctx, char* line) { line = TrimWhitespace(line); } + // Parse: module[@version] [config_path] char* at_sign = strchr(line, '@'); char module_name[DMOD_DEPENDENCIES_MAX_NAME_LEN]; char module_version[DMOD_DEPENDENCIES_MAX_VERSION_LEN] = {0}; + char module_config[DMOD_DEPENDENCIES_MAX_CONFIG_LEN] = {0}; + char* rest_of_line = NULL; if (at_sign) { // Has version @@ -245,19 +254,73 @@ static bool ParseLine(Dmod_DependenciesContext_t* ctx, char* line) { char* trimmed_name = TrimWhitespace(module_name); memmove(module_name, trimmed_name, strlen(trimmed_name) + 1); - // Get version - char* version = at_sign + 1; - version = TrimWhitespace(version); - strncpy(module_version, version, sizeof(module_version) - 1); - module_version[sizeof(module_version) - 1] = '\0'; + // Get version and possibly config + rest_of_line = at_sign + 1; } else { - // No version - strncpy(module_name, line, sizeof(module_name) - 1); - module_name[sizeof(module_name) - 1] = '\0'; - - // Trim the name - char* trimmed_name = TrimWhitespace(module_name); - memmove(module_name, trimmed_name, strlen(trimmed_name) + 1); + // No version, might have config + rest_of_line = line; + } + + // Parse version and config from rest_of_line + if (rest_of_line && at_sign) { + // We have version, parse it and check for config + char* space = strchr(rest_of_line, ' '); + if (space) { + // We have both version and config + size_t version_len = space - rest_of_line; + if (version_len >= sizeof(module_version)) { + SetError(ctx, "Version string too long"); + if (manifest_for_entry) Dmod_Free(manifest_for_entry); + return false; + } + strncpy(module_version, rest_of_line, version_len); + module_version[version_len] = '\0'; + + // Get config path + char* config = TrimWhitespace(space + 1); + if (config[0] != '\0') { + strncpy(module_config, config, sizeof(module_config) - 1); + module_config[sizeof(module_config) - 1] = '\0'; + } + } else { + // Only version + char* version = TrimWhitespace(rest_of_line); + strncpy(module_version, version, sizeof(module_version) - 1); + module_version[sizeof(module_version) - 1] = '\0'; + } + } else if (rest_of_line) { + // No @ sign, parse module name and possibly config + char* space = strchr(rest_of_line, ' '); + if (space) { + // We have both module name and config + size_t name_len = space - rest_of_line; + if (name_len >= sizeof(module_name)) { + SetError(ctx, "Module name too long: %s", line); + if (manifest_for_entry) Dmod_Free(manifest_for_entry); + return false; + } + strncpy(module_name, rest_of_line, name_len); + module_name[name_len] = '\0'; + + // Trim the name + char* trimmed_name = TrimWhitespace(module_name); + memmove(module_name, trimmed_name, strlen(trimmed_name) + 1); + + // Get config path + char* config = TrimWhitespace(space + 1); + if (config[0] != '\0') { + strncpy(module_config, config, sizeof(module_config) - 1); + module_config[sizeof(module_config) - 1] = '\0'; + } + } else { + // Only module name + strncpy(module_name, rest_of_line, sizeof(module_name) - 1); + module_name[sizeof(module_name) - 1] = '\0'; + + // Trim the name + char* trimmed_name = TrimWhitespace(module_name); + memmove(module_name, trimmed_name, strlen(trimmed_name) + 1); + } } // Validate module name @@ -270,7 +333,8 @@ static bool ParseLine(Dmod_DependenciesContext_t* ctx, char* line) { // Use inline manifest if provided, otherwise use current manifest const char* manifest = manifest_for_entry ? manifest_for_entry : (ctx->current_manifest ? ctx->current_manifest : ctx->default_manifest); - bool result = AddEntry(ctx, module_name, module_version[0] ? module_version : NULL, manifest); + bool result = AddEntry(ctx, module_name, module_version[0] ? module_version : NULL, manifest, + module_config[0] ? module_config : NULL); // Free the inline manifest memory after adding the entry if (manifest_for_entry) { diff --git a/tools/lib/dmod_dependencies/dmod_dependencies.h b/tools/lib/dmod_dependencies/dmod_dependencies.h index ff750fde..17d297c5 100644 --- a/tools/lib/dmod_dependencies/dmod_dependencies.h +++ b/tools/lib/dmod_dependencies/dmod_dependencies.h @@ -39,6 +39,11 @@ extern "C" { */ #define DMOD_DEPENDENCIES_MAX_URL_LEN 512 +/** + * @brief Maximum length for configuration paths + */ +#define DMOD_DEPENDENCIES_MAX_CONFIG_LEN 256 + /** * @brief Represents a single dependency entry */ @@ -46,6 +51,7 @@ typedef struct { char name[DMOD_DEPENDENCIES_MAX_NAME_LEN]; /**< Module name */ char version[DMOD_DEPENDENCIES_MAX_VERSION_LEN]; /**< Module version or constraint (empty if not specified) */ char manifest[DMOD_DEPENDENCIES_MAX_URL_LEN]; /**< Manifest URL to use for this module */ + char config[DMOD_DEPENDENCIES_MAX_CONFIG_LEN]; /**< Configuration file path (empty if not specified) */ Dmod_VersionConstraint_t constraint; /**< Parsed version constraint */ bool has_constraint; /**< Whether constraint is parsed and valid */ } Dmod_DependencyEntry_t; diff --git a/tools/system/dmf-get/main.c b/tools/system/dmf-get/main.c index 482c6645..922611b0 100644 --- a/tools/system/dmf-get/main.c +++ b/tools/system/dmf-get/main.c @@ -839,6 +839,109 @@ static bool ExtractResourceFromZip(const char* zip_path, const char* output_dir, return true; } +/** + * @brief Copy configuration file from module package to specified destination + * + * @param module_name Name of the module + * @param config_path Relative path to configuration file within module's config directory + * @param output_dir Output directory where module was installed + * @param config_dest_dir Destination directory for the configuration file + * @return true if configuration file was copied successfully, false otherwise + */ +static bool CopyConfigurationFile(const char* module_name, const char* config_path, + const char* output_dir, const char* config_dest_dir) { + if (!module_name || !config_path || !output_dir || !config_dest_dir) { + DMOD_LOG_ERROR("Invalid parameters for configuration file copy\n"); + return false; + } + + DMOD_LOG_INFO("Looking for configuration file: %s for module: %s\n", config_path, module_name); + + // First, check if the module has a .dmr file in the output directory + char dmr_path[1024]; + Dmod_SnPrintf(dmr_path, sizeof(dmr_path), "%s/%s.dmr", output_dir, module_name); + + char config_source[1024] = ""; + + // Try to find config directory path from .dmr file + if (Dmod_Access(dmr_path, DMOD_R_OK) == 0) { + Dmod_ResourceContext_t* res_ctx = Dmod_Resource_Init(output_dir, module_name); + if (res_ctx) { + if (Dmod_Resource_ParseFile(res_ctx, dmr_path)) { + size_t res_count = Dmod_Resource_GetEntryCount(res_ctx); + for (size_t i = 0; i < res_count; i++) { + Dmod_ResourceEntry_t res_entry; + if (Dmod_Resource_GetEntry(res_ctx, i, &res_entry)) { + // Look for "config" resource entry + if (strcmp(res_entry.key, "config") == 0 || + strcmp(res_entry.key, "configs") == 0) { + // Build full path: destination from .dmr + config_path + Dmod_SnPrintf(config_source, sizeof(config_source), "%s/%s", + res_entry.destination, config_path); + DMOD_LOG_INFO("Found config directory in .dmr: %s\n", res_entry.destination); + break; + } + } + } + } + Dmod_Resource_Free(res_ctx); + } + } + + // If not found in .dmr, try default location: output_dir/module_name/config/ + if (config_source[0] == '\0') { + Dmod_SnPrintf(config_source, sizeof(config_source), "%s/%s/config/%s", + output_dir, module_name, config_path); + DMOD_LOG_INFO("Using default config location: %s\n", config_source); + } + + // Validate source path for safety + if (!IsPathSafe(config_source) || !IsPathSafe(config_dest_dir)) { + DMOD_LOG_ERROR("Invalid configuration path (contains unsafe characters)\n"); + return false; + } + + // Check if source file exists + if (Dmod_Access(config_source, DMOD_R_OK) != 0) { + DMOD_LOG_ERROR("Configuration file not found: %s\n", config_source); + return false; + } + + // Create destination directory if needed + char mkdir_cmd[2048]; + Dmod_SnPrintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p \"%s\"", config_dest_dir); + int mkdir_result = system(mkdir_cmd); + if (mkdir_result != 0) { + DMOD_LOG_ERROR("Failed to create configuration destination directory: %s\n", config_dest_dir); + return false; + } + + // Extract filename from config_path + const char* filename = strrchr(config_path, '/'); + if (filename) { + filename++; // Skip the '/' + } else { + filename = config_path; + } + + // Build full destination path + char config_dest[1024]; + Dmod_SnPrintf(config_dest, sizeof(config_dest), "%s/%s", config_dest_dir, filename); + + // Copy the configuration file + char cp_cmd[2048]; + Dmod_SnPrintf(cp_cmd, sizeof(cp_cmd), "cp \"%s\" \"%s\"", config_source, config_dest); + + int cp_result = system(cp_cmd); + if (cp_result == 0) { + DMOD_LOG_INFO("Configuration file copied successfully to: %s\n", config_dest); + return true; + } else { + DMOD_LOG_ERROR("Failed to copy configuration file\n"); + return false; + } +} + /** * @brief Display license file content to the user * @@ -1623,6 +1726,7 @@ static void PrintUsage(const char* app_name) { Dmod_Printf(" -d, --dependencies Path or URL to dependencies (.dmd) file\n"); Dmod_Printf(" -m, --manifest Path or URL to manifest file\n"); Dmod_Printf(" -o, --output-dir Output directory for downloaded modules\n"); + Dmod_Printf(" --config-dir Directory where configuration files should be copied\n"); Dmod_Printf(" -t, --tools-name Tools name for variable substitution\n"); Dmod_Printf(" -a, --arch-name Architecture name for variable substitution\n"); Dmod_Printf(" --cpu-name CPU name for variable substitution (e.g., stm32f746ngh6)\n"); @@ -1656,6 +1760,7 @@ static void PrintUsage(const char* app_name) { Dmod_Printf(" %s mymodule@>=1.0 # Download version >= 1.0\n", app_name); Dmod_Printf(" %s mymodule@>=1.0<=2.0 # Download version in range [1.0, 2.0]\n", app_name); Dmod_Printf(" %s -d deps.dmd # Download all modules from deps.dmd\n", app_name); + Dmod_Printf(" %s -d deps.dmd --config-dir ./config # Download modules and copy configs to ./config\n", app_name); Dmod_Printf(" %s -m http://... module # Use custom manifest\n", app_name); Dmod_Printf(" %s --type dmfc module # Prefer dmfc files\n", app_name); Dmod_Printf(" %s -a armv7-cortex-m7 module # Use arch name directly\n", app_name); @@ -2024,6 +2129,7 @@ int main(int argc, char* argv[]) { const char* dependencies_path = NULL; const char* manifest_path = NULL; const char* output_dir = NULL; + const char* config_dir = NULL; const char* tools_name = NULL; const char* arch_name = NULL; const char* cpu_name = NULL; @@ -2070,6 +2176,13 @@ int main(int argc, char* argv[]) { } output_dir = argv[i]; } + else if (strcmp(argv[i], "--config-dir") == 0) { + if (++i >= argc) { + DMOD_LOG_ERROR("Error: %s requires an argument\n", argv[i-1]); + return 1; + } + config_dir = argv[i]; + } else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--tools-name") == 0) { if (++i >= argc) { DMOD_LOG_ERROR("Error: %s requires an argument\n", argv[i-1]); @@ -2482,6 +2595,12 @@ int main(int argc, char* argv[]) { if (download_result != 0) { counts.failed_count++; + } else if (dep_entry.config[0] != '\0' && config_dir != NULL) { + // If module specifies a configuration file and config_dir is provided, copy it + DMOD_LOG_INFO(" Configuration file specified: %s\n", dep_entry.config); + if (!CopyConfigurationFile(dep_entry.name, dep_entry.config, output_dir, config_dir)) { + DMOD_LOG_WARN(" Failed to copy configuration file (module was installed successfully)\n"); + } } } From 435ab74852181fbfba4a5671d95bc899430bc522 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:43:25 +0000 Subject: [PATCH 3/7] Add tests and examples for configuration file support Co-authored-by: JohnAmadis <17320783+JohnAmadis@users.noreply.github.com> --- tests/lib/tests_dmod_dependencies.cpp | 103 ++++++++++++++++++ .../dmod_dependencies/vscode-dmd/example.dmd | 11 ++ .../lib/dmod_resource/vscode-dmr/example.dmr | 5 + 3 files changed, 119 insertions(+) diff --git a/tests/lib/tests_dmod_dependencies.cpp b/tests/lib/tests_dmod_dependencies.cpp index 2d11396f..d4c1851c 100644 --- a/tests/lib/tests_dmod_dependencies.cpp +++ b/tests/lib/tests_dmod_dependencies.cpp @@ -396,3 +396,106 @@ TEST_F(DmodDependenciesTest, ParseEmptyInlineFromDirective) { Dmod_Dependencies_Free(ctx); } + +// =============================================================== +// Configuration Tests +// =============================================================== + +TEST_F(DmodDependenciesTest, ParseModuleWithConfig) { + Dmod_DependenciesContext_t* ctx = Dmod_Dependencies_Init("https://example.com/manifest.dmm", MockDownloadFunc, nullptr); + ASSERT_NE(ctx, nullptr); + + const char* dependencies = "dmclk@1.0 mcu/stm32f7.ini\n"; + + ASSERT_TRUE(Dmod_Dependencies_Parse(ctx, dependencies)); + EXPECT_EQ(Dmod_Dependencies_GetEntryCount(ctx), 1); + + Dmod_DependencyEntry_t entry; + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 0, &entry)); + EXPECT_STREQ(entry.name, "dmclk"); + EXPECT_STREQ(entry.version, "1.0"); + EXPECT_STREQ(entry.config, "mcu/stm32f7.ini"); + EXPECT_STREQ(entry.manifest, "https://example.com/manifest.dmm"); + + Dmod_Dependencies_Free(ctx); +} + +TEST_F(DmodDependenciesTest, ParseModuleWithoutVersionButWithConfig) { + Dmod_DependenciesContext_t* ctx = Dmod_Dependencies_Init("https://example.com/manifest.dmm", MockDownloadFunc, nullptr); + ASSERT_NE(ctx, nullptr); + + const char* dependencies = "dmclk configs/default.ini\n"; + + ASSERT_TRUE(Dmod_Dependencies_Parse(ctx, dependencies)); + EXPECT_EQ(Dmod_Dependencies_GetEntryCount(ctx), 1); + + Dmod_DependencyEntry_t entry; + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 0, &entry)); + EXPECT_STREQ(entry.name, "dmclk"); + EXPECT_STREQ(entry.version, ""); + EXPECT_STREQ(entry.config, "configs/default.ini"); + EXPECT_STREQ(entry.manifest, "https://example.com/manifest.dmm"); + + Dmod_Dependencies_Free(ctx); +} + +TEST_F(DmodDependenciesTest, ParseMultipleModulesWithConfig) { + Dmod_DependenciesContext_t* ctx = Dmod_Dependencies_Init("https://example.com/manifest.dmm", MockDownloadFunc, nullptr); + ASSERT_NE(ctx, nullptr); + + const char* dependencies = + "dmclk@1.0 mcu/stm32f7.ini\n" + "uart@2.5 uart/config.ini\n" + "spi\n" // No config + "i2c@1.2\n"; // No config + + ASSERT_TRUE(Dmod_Dependencies_Parse(ctx, dependencies)); + EXPECT_EQ(Dmod_Dependencies_GetEntryCount(ctx), 4); + + Dmod_DependencyEntry_t entry; + + // First module with config + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 0, &entry)); + EXPECT_STREQ(entry.name, "dmclk"); + EXPECT_STREQ(entry.version, "1.0"); + EXPECT_STREQ(entry.config, "mcu/stm32f7.ini"); + + // Second module with config + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 1, &entry)); + EXPECT_STREQ(entry.name, "uart"); + EXPECT_STREQ(entry.version, "2.5"); + EXPECT_STREQ(entry.config, "uart/config.ini"); + + // Third module without config + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 2, &entry)); + EXPECT_STREQ(entry.name, "spi"); + EXPECT_STREQ(entry.version, ""); + EXPECT_STREQ(entry.config, ""); + + // Fourth module without config + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 3, &entry)); + EXPECT_STREQ(entry.name, "i2c"); + EXPECT_STREQ(entry.version, "1.2"); + EXPECT_STREQ(entry.config, ""); + + Dmod_Dependencies_Free(ctx); +} + +TEST_F(DmodDependenciesTest, ParseModuleWithConfigAndInlineFrom) { + Dmod_DependenciesContext_t* ctx = Dmod_Dependencies_Init("https://example.com/manifest.dmm", MockDownloadFunc, nullptr); + ASSERT_NE(ctx, nullptr); + + const char* dependencies = "dmclk@1.0 mcu/stm32f7.ini $from https://custom.com/manifest.dmm\n"; + + ASSERT_TRUE(Dmod_Dependencies_Parse(ctx, dependencies)); + EXPECT_EQ(Dmod_Dependencies_GetEntryCount(ctx), 1); + + Dmod_DependencyEntry_t entry; + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 0, &entry)); + EXPECT_STREQ(entry.name, "dmclk"); + EXPECT_STREQ(entry.version, "1.0"); + EXPECT_STREQ(entry.config, "mcu/stm32f7.ini"); + EXPECT_STREQ(entry.manifest, "https://custom.com/manifest.dmm"); + + Dmod_Dependencies_Free(ctx); +} diff --git a/tools/lib/dmod_dependencies/vscode-dmd/example.dmd b/tools/lib/dmod_dependencies/vscode-dmd/example.dmd index d7a13f16..aedc5ab7 100644 --- a/tools/lib/dmod_dependencies/vscode-dmd/example.dmd +++ b/tools/lib/dmod_dependencies/vscode-dmd/example.dmd @@ -11,6 +11,14 @@ spi@1.0 i2c@2.5 uart@1.2.3 +# Module entries with configuration files +# Format: module[@version] config_path +dmclk@1.0 mcu/stm32f7.ini +uart@1.2.3 configs/uart_115200.ini + +# Module with configuration but no version +gpio configs/gpio_default.cfg + # Module entries with version ranges dmffs_extended@>=1.0 # Version 1.0 or newer old_driver@<=2.0 # Version 2.0 or older @@ -35,3 +43,6 @@ display_lib@4.5 # Inline $from directive for a single module crypto_module@2.1 $from https://security-registry.com/manifest.dmm + +# Module with configuration and inline $from +adc@1.5 config/adc_12bit.ini $from https://custom-registry.com/manifest.dmm diff --git a/tools/lib/dmod_resource/vscode-dmr/example.dmr b/tools/lib/dmod_resource/vscode-dmr/example.dmr index de93b86e..8b099ccb 100644 --- a/tools/lib/dmod_resource/vscode-dmr/example.dmr +++ b/tools/lib/dmod_resource/vscode-dmr/example.dmr @@ -7,6 +7,11 @@ dmf=./module.dmf => ${DMOD_DMF_DIR}/${module}.dmf # Install dependencies file dmd=./module.dmd => ${destination}/${module}.dmd +# Install configuration directory +config=./configs => ${destination}/${module}/config +# or +configs=./configs => ${destination}/${module}/configs + # Install documentation folder docs=./module/docs => ${destination}/${module}/docs From 4575f96a273e0f71a3ab69906341c948d2e96b5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:45:15 +0000 Subject: [PATCH 4/7] Address code review feedback: initialize config field and use named constants Co-authored-by: JohnAmadis <17320783+JohnAmadis@users.noreply.github.com> --- .../lib/dmod_dependencies/dmod_dependencies.c | 2 ++ tools/system/dmf-get/main.c | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/tools/lib/dmod_dependencies/dmod_dependencies.c b/tools/lib/dmod_dependencies/dmod_dependencies.c index ebe0bbad..3b9d50d8 100644 --- a/tools/lib/dmod_dependencies/dmod_dependencies.c +++ b/tools/lib/dmod_dependencies/dmod_dependencies.c @@ -90,6 +90,8 @@ static bool AddEntry(Dmod_DependenciesContext_t* ctx, const char* name, if (config) { strncpy(node->entry.config, config, DMOD_DEPENDENCIES_MAX_CONFIG_LEN - 1); node->entry.config[DMOD_DEPENDENCIES_MAX_CONFIG_LEN - 1] = '\0'; + } else { + node->entry.config[0] = '\0'; } // Add to list diff --git a/tools/system/dmf-get/main.c b/tools/system/dmf-get/main.c index 922611b0..c508af34 100644 --- a/tools/system/dmf-get/main.c +++ b/tools/system/dmf-get/main.c @@ -43,6 +43,10 @@ #define CACHE_MANIFESTS_SUBDIR "manifests" #define CACHE_PACKAGES_SUBDIR "packages" +// Buffer sizes +#define DMOD_MAX_PATH_LEN 1024 +#define DMOD_MAX_CMD_LEN 2048 + // Global cache directory static char g_cache_dir[1024] = {0}; static bool g_cache_enabled = true; @@ -858,10 +862,10 @@ static bool CopyConfigurationFile(const char* module_name, const char* config_pa DMOD_LOG_INFO("Looking for configuration file: %s for module: %s\n", config_path, module_name); // First, check if the module has a .dmr file in the output directory - char dmr_path[1024]; + char dmr_path[DMOD_MAX_PATH_LEN]; Dmod_SnPrintf(dmr_path, sizeof(dmr_path), "%s/%s.dmr", output_dir, module_name); - char config_source[1024] = ""; + char config_source[DMOD_MAX_PATH_LEN] = ""; // Try to find config directory path from .dmr file if (Dmod_Access(dmr_path, DMOD_R_OK) == 0) { @@ -895,7 +899,7 @@ static bool CopyConfigurationFile(const char* module_name, const char* config_pa DMOD_LOG_INFO("Using default config location: %s\n", config_source); } - // Validate source path for safety + // Validate paths for safety - prevents command injection through shell metacharacters if (!IsPathSafe(config_source) || !IsPathSafe(config_dest_dir)) { DMOD_LOG_ERROR("Invalid configuration path (contains unsafe characters)\n"); return false; @@ -908,7 +912,8 @@ static bool CopyConfigurationFile(const char* module_name, const char* config_pa } // Create destination directory if needed - char mkdir_cmd[2048]; + // Note: Using system() with validated paths is consistent with the codebase pattern + char mkdir_cmd[DMOD_MAX_CMD_LEN]; Dmod_SnPrintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p \"%s\"", config_dest_dir); int mkdir_result = system(mkdir_cmd); if (mkdir_result != 0) { @@ -925,11 +930,12 @@ static bool CopyConfigurationFile(const char* module_name, const char* config_pa } // Build full destination path - char config_dest[1024]; + char config_dest[DMOD_MAX_PATH_LEN]; Dmod_SnPrintf(config_dest, sizeof(config_dest), "%s/%s", config_dest_dir, filename); // Copy the configuration file - char cp_cmd[2048]; + // Note: Using system() with validated paths is consistent with the codebase pattern + char cp_cmd[DMOD_MAX_CMD_LEN]; Dmod_SnPrintf(cp_cmd, sizeof(cp_cmd), "cp \"%s\" \"%s\"", config_source, config_dest); int cp_result = system(cp_cmd); From ac194ef45f2174090935a551603c703ec2df5714 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:49:36 +0000 Subject: [PATCH 5/7] Add documentation for configuration file feature Co-authored-by: JohnAmadis <17320783+JohnAmadis@users.noreply.github.com> --- docs/dmd-file-format.md | 45 ++++++++++++++++++++++++++++++++++++++++- docs/dmr-file-format.md | 23 +++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/dmd-file-format.md b/docs/dmd-file-format.md index 99926961..0a032212 100644 --- a/docs/dmd-file-format.md +++ b/docs/dmd-file-format.md @@ -25,7 +25,7 @@ Lines starting with `#` are treated as comments and ignored: ### Module Entries -Modules are specified by name, optionally followed by a version or version constraint: +Modules are specified by name, optionally followed by a version or version constraint, and optionally a configuration file path: ```dmd # Module without version (downloads latest available) @@ -35,6 +35,13 @@ dmffs driver@1.0 spi@2.5.1 +# Module with configuration file +dmclk@1.0 mcu/stm32f7.ini +uart@2.5 configs/uart_115200.ini + +# Module with configuration but no version +gpio configs/gpio_default.cfg + # Module with version range - all versions >= 1.0 dmffs@>=1.0 @@ -49,6 +56,32 @@ i2c@>1.0 # Greater than 1.0 (exclusive) can@<2.0 # Less than 2.0 (exclusive) ``` +#### Configuration Files + +Modules can optionally specify a configuration file that should be copied during installation. The configuration file path is relative to the module's configuration directory (as defined in the module's `.dmr` file, or the default `config/` directory). + +Format: `module[@version] [config_path]` + +Example: +```dmd +# Install dmclk version 1.0 with mcu/stm32f7.ini configuration +dmclk@1.0 mcu/stm32f7.ini + +# Install uart with configuration but use latest version +uart configs/uart_default.ini +``` + +To copy the configuration files during installation, use the `--config-dir` option with dmf-get: + +```bash +# Install modules and copy configuration files to ./config directory +dmf-get -d project-deps.dmd --config-dir ./config +``` + +The configuration file will be looked up in: +1. The path specified in the module's `.dmr` file with the `config` or `configs` resource key +2. If not found, the default location: `//config/` + #### Version Constraint Syntax Version constraints support the following operators: @@ -95,6 +128,16 @@ module2@2.0 $from https://special-registry.com/manifest.dmm module3@1.5 ``` +Configuration files can be combined with inline `$from`: + +```dmd +# Module with configuration and custom manifest +dmclk@1.0 mcu/stm32f7.ini $from https://hw-registry.com/manifest.dmm + +# Configuration with default manifest +uart@2.5 configs/uart.ini +``` + ### Include Directive The `$include` directive allows you to include another `.dmd` file: diff --git a/docs/dmr-file-format.md b/docs/dmr-file-format.md index b3d82d5c..3aa95d00 100644 --- a/docs/dmr-file-format.md +++ b/docs/dmr-file-format.md @@ -132,6 +132,29 @@ api_header=./api.h => ${destination}/${module}/include/api.h dmd=./module.dmd => ${destination}/${module}.dmd ``` +#### Configuration Files + +Configuration directories can be specified so that individual configuration files can be copied during module installation: + +```dmr +# Configuration directory +config=./configs => ${destination}/${module}/config + +# Alternative naming +configs=./config-files => ${destination}/${module}/configs +``` + +When a module specifies a configuration file in the `.dmd` file (e.g., `dmclk@1.0 mcu/stm32f7.ini`), dmf-get will look for that file in: +1. The path specified by the `config` or `configs` resource in the `.dmr` file +2. If not found, the default location: `${destination}/${module}/config/` + +Configuration files are copied when using the `--config-dir` option with dmf-get: + +```bash +# Install modules and copy specified configuration files +dmf-get -d project-deps.dmd --config-dir ./project-config +``` + #### License and Legal ```dmr From d7c8816a2baf7876405693a3324ca6cacb6a4ad1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:11:19 +0000 Subject: [PATCH 6/7] Add custom destination naming and multiple config support per user feedback Co-authored-by: JohnAmadis <17320783+JohnAmadis@users.noreply.github.com> --- docs/dmd-file-format.md | 28 ++- tests/lib/tests_dmod_dependencies.cpp | 76 +++++++++ .../lib/dmod_dependencies/dmod_dependencies.c | 159 +++++++++++------- .../lib/dmod_dependencies/dmod_dependencies.h | 1 + .../dmod_dependencies/vscode-dmd/example.dmd | 15 +- tools/system/dmf-get/main.c | 86 +++++++--- 6 files changed, 284 insertions(+), 81 deletions(-) diff --git a/docs/dmd-file-format.md b/docs/dmd-file-format.md index 0a032212..b8bba9ef 100644 --- a/docs/dmd-file-format.md +++ b/docs/dmd-file-format.md @@ -60,17 +60,35 @@ can@<2.0 # Less than 2.0 (exclusive) Modules can optionally specify a configuration file that should be copied during installation. The configuration file path is relative to the module's configuration directory (as defined in the module's `.dmr` file, or the default `config/` directory). -Format: `module[@version] [config_path]` +**Format**: `module[@version] [config_path] [custom_dest_name]` -Example: +**Basic usage**: ```dmd # Install dmclk version 1.0 with mcu/stm32f7.ini configuration +# Copies to: /dmclk/stm32f7.ini dmclk@1.0 mcu/stm32f7.ini # Install uart with configuration but use latest version uart configs/uart_default.ini ``` +**Custom destination filename**: +```dmd +# Specify custom destination filename (no module subdirectory) +# Copies to: /clk.ini +dmclk@1.0 mcu/stm32f7.ini clk.ini +``` + +**Multiple configurations from same driver**: +```dmd +# Use driver version 1.0, but copy configs from different versions +dmclk@1.0 # Install driver v1.0 +dmclk@0.1 mcu/stm32f7.ini # Copy config from v0.1 to dmclk/stm32f7.ini +dmclk@0.2 mcu/high-speed.ini # Copy config from v0.2 to dmclk/high-speed.ini +``` + +This allows using a specific driver version while accessing configuration files from multiple versions of the module. + To copy the configuration files during installation, use the `--config-dir` option with dmf-get: ```bash @@ -78,10 +96,14 @@ To copy the configuration files during installation, use the `--config-dir` opti dmf-get -d project-deps.dmd --config-dir ./config ``` -The configuration file will be looked up in: +**Configuration file lookup**: 1. The path specified in the module's `.dmr` file with the `config` or `configs` resource key 2. If not found, the default location: `//config/` +**Destination naming**: +- **Default**: `//` +- **With custom name**: `/` + #### Version Constraint Syntax Version constraints support the following operators: diff --git a/tests/lib/tests_dmod_dependencies.cpp b/tests/lib/tests_dmod_dependencies.cpp index d4c1851c..08a9aff7 100644 --- a/tests/lib/tests_dmod_dependencies.cpp +++ b/tests/lib/tests_dmod_dependencies.cpp @@ -499,3 +499,79 @@ TEST_F(DmodDependenciesTest, ParseModuleWithConfigAndInlineFrom) { Dmod_Dependencies_Free(ctx); } + +TEST_F(DmodDependenciesTest, ParseModuleWithConfigAndCustomDestName) { + Dmod_DependenciesContext_t* ctx = Dmod_Dependencies_Init("https://example.com/manifest.dmm", MockDownloadFunc, nullptr); + ASSERT_NE(ctx, nullptr); + + const char* dependencies = "dmclk@1.0 mcu/stm32f7.ini clk.ini\n"; + + ASSERT_TRUE(Dmod_Dependencies_Parse(ctx, dependencies)); + EXPECT_EQ(Dmod_Dependencies_GetEntryCount(ctx), 1); + + Dmod_DependencyEntry_t entry; + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 0, &entry)); + EXPECT_STREQ(entry.name, "dmclk"); + EXPECT_STREQ(entry.version, "1.0"); + EXPECT_STREQ(entry.config, "mcu/stm32f7.ini"); + EXPECT_STREQ(entry.config_dest, "clk.ini"); + EXPECT_STREQ(entry.manifest, "https://example.com/manifest.dmm"); + + Dmod_Dependencies_Free(ctx); +} + +TEST_F(DmodDependenciesTest, ParseModuleWithConfigNoVersionButWithCustomDest) { + Dmod_DependenciesContext_t* ctx = Dmod_Dependencies_Init("https://example.com/manifest.dmm", MockDownloadFunc, nullptr); + ASSERT_NE(ctx, nullptr); + + const char* dependencies = "dmclk configs/default.ini my-config.ini\n"; + + ASSERT_TRUE(Dmod_Dependencies_Parse(ctx, dependencies)); + EXPECT_EQ(Dmod_Dependencies_GetEntryCount(ctx), 1); + + Dmod_DependencyEntry_t entry; + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 0, &entry)); + EXPECT_STREQ(entry.name, "dmclk"); + EXPECT_STREQ(entry.version, ""); + EXPECT_STREQ(entry.config, "configs/default.ini"); + EXPECT_STREQ(entry.config_dest, "my-config.ini"); + EXPECT_STREQ(entry.manifest, "https://example.com/manifest.dmm"); + + Dmod_Dependencies_Free(ctx); +} + +TEST_F(DmodDependenciesTest, ParseMultipleEntriesSameDriverDifferentConfigs) { + Dmod_DependenciesContext_t* ctx = Dmod_Dependencies_Init("https://example.com/manifest.dmm", MockDownloadFunc, nullptr); + ASSERT_NE(ctx, nullptr); + + // Test case from user comment: same driver with different versions for configs + const char* dependencies = + "dmclk@1.0\n" + "dmclk@0.1 mcu/stm32f7.ini\n" + "dmclk@0.2 mcu/high-speed.ini\n"; + + ASSERT_TRUE(Dmod_Dependencies_Parse(ctx, dependencies)); + EXPECT_EQ(Dmod_Dependencies_GetEntryCount(ctx), 3); + + Dmod_DependencyEntry_t entry; + + // First entry: driver only + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 0, &entry)); + EXPECT_STREQ(entry.name, "dmclk"); + EXPECT_STREQ(entry.version, "1.0"); + EXPECT_STREQ(entry.config, ""); + + // Second entry: same driver with config from version 0.1 + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 1, &entry)); + EXPECT_STREQ(entry.name, "dmclk"); + EXPECT_STREQ(entry.version, "0.1"); + EXPECT_STREQ(entry.config, "mcu/stm32f7.ini"); + + // Third entry: same driver with config from version 0.2 + ASSERT_TRUE(Dmod_Dependencies_GetEntry(ctx, 2, &entry)); + EXPECT_STREQ(entry.name, "dmclk"); + EXPECT_STREQ(entry.version, "0.2"); + EXPECT_STREQ(entry.config, "mcu/high-speed.ini"); + + Dmod_Dependencies_Free(ctx); +} diff --git a/tools/lib/dmod_dependencies/dmod_dependencies.c b/tools/lib/dmod_dependencies/dmod_dependencies.c index 3b9d50d8..010a1e4c 100644 --- a/tools/lib/dmod_dependencies/dmod_dependencies.c +++ b/tools/lib/dmod_dependencies/dmod_dependencies.c @@ -60,7 +60,8 @@ static void SetError(Dmod_DependenciesContext_t* ctx, const char* format, ...) { * @brief Add a dependency entry to the list */ static bool AddEntry(Dmod_DependenciesContext_t* ctx, const char* name, - const char* version, const char* manifest, const char* config) { + const char* version, const char* manifest, const char* config, + const char* config_dest) { DependencyNode_t* node = (DependencyNode_t*)Dmod_Malloc(sizeof(DependencyNode_t)); if (!node) { SetError(ctx, "Out of memory"); @@ -94,6 +95,14 @@ static bool AddEntry(Dmod_DependenciesContext_t* ctx, const char* name, node->entry.config[0] = '\0'; } + // Copy config destination if provided + if (config_dest) { + strncpy(node->entry.config_dest, config_dest, DMOD_DEPENDENCIES_MAX_CONFIG_LEN - 1); + node->entry.config_dest[DMOD_DEPENDENCIES_MAX_CONFIG_LEN - 1] = '\0'; + } else { + node->entry.config_dest[0] = '\0'; + } + // Add to list if (ctx->tail) { ctx->tail->next = node; @@ -233,15 +242,18 @@ static bool ParseLine(Dmod_DependenciesContext_t* ctx, char* line) { line = TrimWhitespace(line); } - // Parse: module[@version] [config_path] + // Parse: module[@version] [config_path] [config_dest] char* at_sign = strchr(line, '@'); char module_name[DMOD_DEPENDENCIES_MAX_NAME_LEN]; char module_version[DMOD_DEPENDENCIES_MAX_VERSION_LEN] = {0}; char module_config[DMOD_DEPENDENCIES_MAX_CONFIG_LEN] = {0}; - char* rest_of_line = NULL; + char module_config_dest[DMOD_DEPENDENCIES_MAX_CONFIG_LEN] = {0}; - if (at_sign) { - // Has version + // First, extract module name (and version if present) + char* first_space = strchr(line, ' '); + + if (at_sign && (!first_space || at_sign < first_space)) { + // Has version: module@version ... size_t name_len = at_sign - line; if (name_len >= sizeof(module_name)) { SetError(ctx, "Module name too long: %s", line); @@ -256,73 +268,103 @@ static bool ParseLine(Dmod_DependenciesContext_t* ctx, char* line) { char* trimmed_name = TrimWhitespace(module_name); memmove(module_name, trimmed_name, strlen(trimmed_name) + 1); - // Get version and possibly config - rest_of_line = at_sign + 1; - } else { - // No version, might have config - rest_of_line = line; - } - - // Parse version and config from rest_of_line - if (rest_of_line && at_sign) { - // We have version, parse it and check for config - char* space = strchr(rest_of_line, ' '); - if (space) { - // We have both version and config - size_t version_len = space - rest_of_line; + // Extract version (up to first space or end of string) + char* version_start = at_sign + 1; + char* version_end = strchr(version_start, ' '); + + if (version_end) { + size_t version_len = version_end - version_start; if (version_len >= sizeof(module_version)) { SetError(ctx, "Version string too long"); if (manifest_for_entry) Dmod_Free(manifest_for_entry); return false; } - strncpy(module_version, rest_of_line, version_len); + strncpy(module_version, version_start, version_len); module_version[version_len] = '\0'; - // Get config path - char* config = TrimWhitespace(space + 1); - if (config[0] != '\0') { - strncpy(module_config, config, sizeof(module_config) - 1); - module_config[sizeof(module_config) - 1] = '\0'; + // Parse config and optional config_dest after version + char* config_start = TrimWhitespace(version_end + 1); + if (config_start[0] != '\0') { + char* config_space = strchr(config_start, ' '); + if (config_space) { + // Both config and config_dest + size_t config_len = config_space - config_start; + if (config_len >= sizeof(module_config)) { + SetError(ctx, "Config path too long"); + if (manifest_for_entry) Dmod_Free(manifest_for_entry); + return false; + } + strncpy(module_config, config_start, config_len); + module_config[config_len] = '\0'; + + // Get config_dest + char* config_dest = TrimWhitespace(config_space + 1); + if (config_dest[0] != '\0') { + strncpy(module_config_dest, config_dest, sizeof(module_config_dest) - 1); + module_config_dest[sizeof(module_config_dest) - 1] = '\0'; + } + } else { + // Only config + strncpy(module_config, config_start, sizeof(module_config) - 1); + module_config[sizeof(module_config) - 1] = '\0'; + } } } else { - // Only version - char* version = TrimWhitespace(rest_of_line); + // Version goes to end of string + char* version = TrimWhitespace(version_start); strncpy(module_version, version, sizeof(module_version) - 1); module_version[sizeof(module_version) - 1] = '\0'; } - } else if (rest_of_line) { - // No @ sign, parse module name and possibly config - char* space = strchr(rest_of_line, ' '); - if (space) { - // We have both module name and config - size_t name_len = space - rest_of_line; - if (name_len >= sizeof(module_name)) { - SetError(ctx, "Module name too long: %s", line); - if (manifest_for_entry) Dmod_Free(manifest_for_entry); - return false; - } - strncpy(module_name, rest_of_line, name_len); - module_name[name_len] = '\0'; - - // Trim the name - char* trimmed_name = TrimWhitespace(module_name); - memmove(module_name, trimmed_name, strlen(trimmed_name) + 1); - - // Get config path - char* config = TrimWhitespace(space + 1); - if (config[0] != '\0') { - strncpy(module_config, config, sizeof(module_config) - 1); + } else if (first_space) { + // No version, but has space: module config_path [config_dest] + size_t name_len = first_space - line; + if (name_len >= sizeof(module_name)) { + SetError(ctx, "Module name too long: %s", line); + if (manifest_for_entry) Dmod_Free(manifest_for_entry); + return false; + } + strncpy(module_name, line, name_len); + module_name[name_len] = '\0'; + + // Trim the name + char* trimmed_name = TrimWhitespace(module_name); + memmove(module_name, trimmed_name, strlen(trimmed_name) + 1); + + // Parse config and optional config_dest + char* config_start = TrimWhitespace(first_space + 1); + if (config_start[0] != '\0') { + char* config_space = strchr(config_start, ' '); + if (config_space) { + // Both config and config_dest + size_t config_len = config_space - config_start; + if (config_len >= sizeof(module_config)) { + SetError(ctx, "Config path too long"); + if (manifest_for_entry) Dmod_Free(manifest_for_entry); + return false; + } + strncpy(module_config, config_start, config_len); + module_config[config_len] = '\0'; + + // Get config_dest + char* config_dest = TrimWhitespace(config_space + 1); + if (config_dest[0] != '\0') { + strncpy(module_config_dest, config_dest, sizeof(module_config_dest) - 1); + module_config_dest[sizeof(module_config_dest) - 1] = '\0'; + } + } else { + // Only config + strncpy(module_config, config_start, sizeof(module_config) - 1); module_config[sizeof(module_config) - 1] = '\0'; } - } else { - // Only module name - strncpy(module_name, rest_of_line, sizeof(module_name) - 1); - module_name[sizeof(module_name) - 1] = '\0'; - - // Trim the name - char* trimmed_name = TrimWhitespace(module_name); - memmove(module_name, trimmed_name, strlen(trimmed_name) + 1); } + } else { + // Only module name + strncpy(module_name, line, sizeof(module_name) - 1); + module_name[sizeof(module_name) - 1] = '\0'; + + // Trim the name + char* trimmed_name = TrimWhitespace(module_name); + memmove(module_name, trimmed_name, strlen(trimmed_name) + 1); } // Validate module name @@ -336,7 +378,8 @@ static bool ParseLine(Dmod_DependenciesContext_t* ctx, char* line) { const char* manifest = manifest_for_entry ? manifest_for_entry : (ctx->current_manifest ? ctx->current_manifest : ctx->default_manifest); bool result = AddEntry(ctx, module_name, module_version[0] ? module_version : NULL, manifest, - module_config[0] ? module_config : NULL); + module_config[0] ? module_config : NULL, + module_config_dest[0] ? module_config_dest : NULL); // Free the inline manifest memory after adding the entry if (manifest_for_entry) { diff --git a/tools/lib/dmod_dependencies/dmod_dependencies.h b/tools/lib/dmod_dependencies/dmod_dependencies.h index 17d297c5..2e958249 100644 --- a/tools/lib/dmod_dependencies/dmod_dependencies.h +++ b/tools/lib/dmod_dependencies/dmod_dependencies.h @@ -52,6 +52,7 @@ typedef struct { char version[DMOD_DEPENDENCIES_MAX_VERSION_LEN]; /**< Module version or constraint (empty if not specified) */ char manifest[DMOD_DEPENDENCIES_MAX_URL_LEN]; /**< Manifest URL to use for this module */ char config[DMOD_DEPENDENCIES_MAX_CONFIG_LEN]; /**< Configuration file path (empty if not specified) */ + char config_dest[DMOD_DEPENDENCIES_MAX_CONFIG_LEN]; /**< Custom destination filename for config (empty if not specified) */ Dmod_VersionConstraint_t constraint; /**< Parsed version constraint */ bool has_constraint; /**< Whether constraint is parsed and valid */ } Dmod_DependencyEntry_t; diff --git a/tools/lib/dmod_dependencies/vscode-dmd/example.dmd b/tools/lib/dmod_dependencies/vscode-dmd/example.dmd index aedc5ab7..5c3287a1 100644 --- a/tools/lib/dmod_dependencies/vscode-dmd/example.dmd +++ b/tools/lib/dmod_dependencies/vscode-dmd/example.dmd @@ -12,13 +12,23 @@ i2c@2.5 uart@1.2.3 # Module entries with configuration files -# Format: module[@version] config_path +# Format: module[@version] config_path [custom_dest_name] dmclk@1.0 mcu/stm32f7.ini uart@1.2.3 configs/uart_115200.ini # Module with configuration but no version gpio configs/gpio_default.cfg +# Module with custom destination filename +# This will create clk.ini directly in config dir (without dmclk/ subdirectory) +dmclk@1.0 mcu/stm32f7.ini clk.ini + +# Multiple entries for same driver with different config versions +# Use driver from 1.0, but copy configs from different versions +dmclk@1.0 +dmclk@0.1 mcu/stm32f7.ini +dmclk@0.2 mcu/high-speed.ini + # Module entries with version ranges dmffs_extended@>=1.0 # Version 1.0 or newer old_driver@<=2.0 # Version 2.0 or older @@ -46,3 +56,6 @@ crypto_module@2.1 $from https://security-registry.com/manifest.dmm # Module with configuration and inline $from adc@1.5 config/adc_12bit.ini $from https://custom-registry.com/manifest.dmm + +# Configuration with custom name and inline $from +dac@2.0 config/dac_default.ini my_dac.ini $from https://hw-registry.com/manifest.dmm diff --git a/tools/system/dmf-get/main.c b/tools/system/dmf-get/main.c index c508af34..21f7be82 100644 --- a/tools/system/dmf-get/main.c +++ b/tools/system/dmf-get/main.c @@ -850,10 +850,12 @@ static bool ExtractResourceFromZip(const char* zip_path, const char* output_dir, * @param config_path Relative path to configuration file within module's config directory * @param output_dir Output directory where module was installed * @param config_dest_dir Destination directory for the configuration file + * @param custom_dest_name Optional custom destination filename (can be NULL) * @return true if configuration file was copied successfully, false otherwise */ static bool CopyConfigurationFile(const char* module_name, const char* config_path, - const char* output_dir, const char* config_dest_dir) { + const char* output_dir, const char* config_dest_dir, + const char* custom_dest_name) { if (!module_name || !config_path || !output_dir || !config_dest_dir) { DMOD_LOG_ERROR("Invalid parameters for configuration file copy\n"); return false; @@ -911,27 +913,72 @@ static bool CopyConfigurationFile(const char* module_name, const char* config_pa return false; } - // Create destination directory if needed - // Note: Using system() with validated paths is consistent with the codebase pattern - char mkdir_cmd[DMOD_MAX_CMD_LEN]; - Dmod_SnPrintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p \"%s\"", config_dest_dir); - int mkdir_result = system(mkdir_cmd); - if (mkdir_result != 0) { - DMOD_LOG_ERROR("Failed to create configuration destination directory: %s\n", config_dest_dir); - return false; - } + // Build full destination path + char config_dest[DMOD_MAX_PATH_LEN]; - // Extract filename from config_path - const char* filename = strrchr(config_path, '/'); - if (filename) { - filename++; // Skip the '/' + if (custom_dest_name && custom_dest_name[0] != '\0') { + // Use custom destination filename (no module subdirectory) + Dmod_SnPrintf(config_dest, sizeof(config_dest), "%s/%s", config_dest_dir, custom_dest_name); } else { - filename = config_path; + // Default: use module_name/filename pattern + // Extract filename from config_path + const char* filename = strrchr(config_path, '/'); + if (filename) { + filename++; // Skip the '/' + } else { + filename = config_path; + } + + // Create module subdirectory path + char module_subdir[DMOD_MAX_PATH_LEN]; + Dmod_SnPrintf(module_subdir, sizeof(module_subdir), "%s/%s", config_dest_dir, module_name); + + // Validate module subdir path + if (!IsPathSafe(module_subdir)) { + DMOD_LOG_ERROR("Invalid module subdirectory path (contains unsafe characters)\n"); + return false; + } + + // Create module subdirectory + char mkdir_cmd[DMOD_MAX_CMD_LEN]; + Dmod_SnPrintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p \"%s\"", module_subdir); + int mkdir_result = system(mkdir_cmd); + if (mkdir_result != 0) { + DMOD_LOG_ERROR("Failed to create module subdirectory: %s\n", module_subdir); + return false; + } + + Dmod_SnPrintf(config_dest, sizeof(config_dest), "%s/%s", module_subdir, filename); } - // Build full destination path - char config_dest[DMOD_MAX_PATH_LEN]; - Dmod_SnPrintf(config_dest, sizeof(config_dest), "%s/%s", config_dest_dir, filename); + // Validate destination path + if (!IsPathSafe(config_dest)) { + DMOD_LOG_ERROR("Invalid destination path (contains unsafe characters)\n"); + return false; + } + + // Create destination directory if needed (parent of config_dest) + char dest_parent[DMOD_MAX_PATH_LEN]; + strncpy(dest_parent, config_dest, sizeof(dest_parent) - 1); + dest_parent[sizeof(dest_parent) - 1] = '\0'; + + char* last_slash = strrchr(dest_parent, '/'); + if (last_slash) { + *last_slash = '\0'; + + if (!IsPathSafe(dest_parent)) { + DMOD_LOG_ERROR("Invalid parent directory path (contains unsafe characters)\n"); + return false; + } + + char mkdir_cmd[DMOD_MAX_CMD_LEN]; + Dmod_SnPrintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p \"%s\"", dest_parent); + int mkdir_result = system(mkdir_cmd); + if (mkdir_result != 0) { + DMOD_LOG_ERROR("Failed to create configuration destination directory: %s\n", dest_parent); + return false; + } + } // Copy the configuration file // Note: Using system() with validated paths is consistent with the codebase pattern @@ -2604,7 +2651,8 @@ int main(int argc, char* argv[]) { } else if (dep_entry.config[0] != '\0' && config_dir != NULL) { // If module specifies a configuration file and config_dir is provided, copy it DMOD_LOG_INFO(" Configuration file specified: %s\n", dep_entry.config); - if (!CopyConfigurationFile(dep_entry.name, dep_entry.config, output_dir, config_dir)) { + const char* custom_dest = (dep_entry.config_dest[0] != '\0') ? dep_entry.config_dest : NULL; + if (!CopyConfigurationFile(dep_entry.name, dep_entry.config, output_dir, config_dir, custom_dest)) { DMOD_LOG_WARN(" Failed to copy configuration file (module was installed successfully)\n"); } } From aacf3ff077a5240150e5f41543233774ee0241b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:13:19 +0000 Subject: [PATCH 7/7] Add variable substitution support for configuration file paths Co-authored-by: JohnAmadis <17320783+JohnAmadis@users.noreply.github.com> --- docs/dmd-file-format.md | 31 +++ .../dmod_dependencies/vscode-dmd/example.dmd | 5 + tools/system/dmf-get/main.c | 199 +++++++++++++++++- 3 files changed, 226 insertions(+), 9 deletions(-) diff --git a/docs/dmd-file-format.md b/docs/dmd-file-format.md index b8bba9ef..78c8ca4b 100644 --- a/docs/dmd-file-format.md +++ b/docs/dmd-file-format.md @@ -89,6 +89,37 @@ dmclk@0.2 mcu/high-speed.ini # Copy config from v0.2 to dmclk/high-speed.in This allows using a specific driver version while accessing configuration files from multiple versions of the module. +**Variable substitution in configuration paths**: + +Configuration paths support variable substitution using `${VARIABLE_NAME}` syntax. Variables can be defined via command-line options or environment variables: + +```dmd +# Use variable in config path +dmclk@1.0 boards/${BOARD}/config.ini + +# Variable in both config path and destination +uart@2.0 boards/${BOARD}/uart.ini ${BOARD}_uart.ini +``` + +Command-line usage: +```bash +# Define variables with -D or --define +dmf-get -d project-deps.dmd --config-dir ./config -D BOARD=stm32f7 + +# Multiple variables +dmf-get -d deps.dmd --config-dir ./config -D BOARD=stm32f7 -D VERSION=v1 +``` + +Variables are substituted in: +- Configuration file paths (source) +- Custom destination filenames + +Variable lookup order: +1. User-defined variables (via `-D` option) +2. Environment variables + +If a variable is not found, the original `${VAR}` syntax is kept in the path. + To copy the configuration files during installation, use the `--config-dir` option with dmf-get: ```bash diff --git a/tools/lib/dmod_dependencies/vscode-dmd/example.dmd b/tools/lib/dmod_dependencies/vscode-dmd/example.dmd index 5c3287a1..3610071b 100644 --- a/tools/lib/dmod_dependencies/vscode-dmd/example.dmd +++ b/tools/lib/dmod_dependencies/vscode-dmd/example.dmd @@ -23,6 +23,11 @@ gpio configs/gpio_default.cfg # This will create clk.ini directly in config dir (without dmclk/ subdirectory) dmclk@1.0 mcu/stm32f7.ini clk.ini +# Variable substitution in configuration paths +# Use with: dmf-get -d deps.dmd --config-dir ./config -D BOARD=stm32f7 +dmclk@1.0 boards/${BOARD}/config.ini +uart@2.0 boards/${BOARD}/uart.ini ${BOARD}_uart.ini + # Multiple entries for same driver with different config versions # Use driver from 1.0, but copy configs from different versions dmclk@1.0 diff --git a/tools/system/dmf-get/main.c b/tools/system/dmf-get/main.c index 21f7be82..2f453069 100644 --- a/tools/system/dmf-get/main.c +++ b/tools/system/dmf-get/main.c @@ -47,6 +47,135 @@ #define DMOD_MAX_PATH_LEN 1024 #define DMOD_MAX_CMD_LEN 2048 +// Maximum number of user-defined variables +#define MAX_USER_VARIABLES 32 + +// Structure to hold user-defined variables +typedef struct { + char name[128]; + char value[512]; +} UserVariable_t; + +// Global array of user-defined variables +static UserVariable_t g_user_variables[MAX_USER_VARIABLES]; +static size_t g_user_variable_count = 0; + +/** + * @brief Add a user-defined variable + * + * @param name Variable name + * @param value Variable value + * @return true if added successfully, false if array is full + */ +static bool AddUserVariable(const char* name, const char* value) { + if (g_user_variable_count >= MAX_USER_VARIABLES) { + return false; + } + + strncpy(g_user_variables[g_user_variable_count].name, name, sizeof(g_user_variables[0].name) - 1); + g_user_variables[g_user_variable_count].name[sizeof(g_user_variables[0].name) - 1] = '\0'; + + strncpy(g_user_variables[g_user_variable_count].value, value, sizeof(g_user_variables[0].value) - 1); + g_user_variables[g_user_variable_count].value[sizeof(g_user_variables[0].value) - 1] = '\0'; + + g_user_variable_count++; + return true; +} + +/** + * @brief Get value of a user-defined variable + * + * @param name Variable name + * @return Variable value or NULL if not found + */ +static const char* GetUserVariable(const char* name) { + for (size_t i = 0; i < g_user_variable_count; i++) { + if (strcmp(g_user_variables[i].name, name) == 0) { + return g_user_variables[i].value; + } + } + return NULL; +} + +/** + * @brief Substitute variables in a string + * + * Replaces ${VAR_NAME} with variable values. + * Checks user-defined variables first, then environment variables. + * + * @param input Input string with variables + * @param output Output buffer + * @param output_size Size of output buffer + * @return true on success, false on buffer overflow + */ +static bool SubstituteConfigVariables(const char* input, char* output, size_t output_size) { + const char* src = input; + char* dst = output; + char* dst_end = output + output_size - 1; + + while (*src && dst < dst_end) { + if (src[0] == '$' && src[1] == '{') { + // Found variable start + const char* var_start = src + 2; + const char* var_end = strchr(var_start, '}'); + + if (!var_end) { + // Malformed variable, copy as-is + *dst++ = *src++; + continue; + } + + // Extract variable name + size_t var_len = var_end - var_start; + char var_name[128]; + if (var_len >= sizeof(var_name)) { + // Variable name too long, copy as-is + *dst++ = *src++; + continue; + } + + strncpy(var_name, var_start, var_len); + var_name[var_len] = '\0'; + + // Substitute variable (check user variables first, then environment) + const char* value = GetUserVariable(var_name); + if (!value) { + value = Dmod_GetEnv(var_name); + } + + if (value) { + size_t value_len = strlen(value); + if (dst + value_len >= dst_end) { + // Buffer overflow + return false; + } + strcpy(dst, value); + dst += value_len; + } else { + // Variable not found, keep the original ${var} syntax + size_t placeholder_len = (var_end - src) + 1; + if (dst + placeholder_len >= dst_end) { + return false; + } + strncpy(dst, src, placeholder_len); + dst += placeholder_len; + } + + src = var_end + 1; + } else { + *dst++ = *src++; + } + } + + if (dst >= dst_end && *src) { + // Buffer overflow + return false; + } + + *dst = '\0'; + return true; +} + // Global cache directory static char g_cache_dir[1024] = {0}; static bool g_cache_enabled = true; @@ -861,7 +990,25 @@ static bool CopyConfigurationFile(const char* module_name, const char* config_pa return false; } - DMOD_LOG_INFO("Looking for configuration file: %s for module: %s\n", config_path, module_name); + // Substitute variables in config_path + char substituted_config_path[DMOD_MAX_PATH_LEN]; + if (!SubstituteConfigVariables(config_path, substituted_config_path, sizeof(substituted_config_path))) { + DMOD_LOG_ERROR("Failed to substitute variables in config path: %s\n", config_path); + return false; + } + + // Also substitute variables in custom_dest_name if provided + char substituted_dest_name[DMOD_MAX_PATH_LEN]; + const char* final_dest_name = NULL; + if (custom_dest_name && custom_dest_name[0] != '\0') { + if (!SubstituteConfigVariables(custom_dest_name, substituted_dest_name, sizeof(substituted_dest_name))) { + DMOD_LOG_ERROR("Failed to substitute variables in custom destination name: %s\n", custom_dest_name); + return false; + } + final_dest_name = substituted_dest_name; + } + + DMOD_LOG_INFO("Looking for configuration file: %s for module: %s\n", substituted_config_path, module_name); // First, check if the module has a .dmr file in the output directory char dmr_path[DMOD_MAX_PATH_LEN]; @@ -881,9 +1028,9 @@ static bool CopyConfigurationFile(const char* module_name, const char* config_pa // Look for "config" resource entry if (strcmp(res_entry.key, "config") == 0 || strcmp(res_entry.key, "configs") == 0) { - // Build full path: destination from .dmr + config_path + // Build full path: destination from .dmr + substituted config_path Dmod_SnPrintf(config_source, sizeof(config_source), "%s/%s", - res_entry.destination, config_path); + res_entry.destination, substituted_config_path); DMOD_LOG_INFO("Found config directory in .dmr: %s\n", res_entry.destination); break; } @@ -897,7 +1044,7 @@ static bool CopyConfigurationFile(const char* module_name, const char* config_pa // If not found in .dmr, try default location: output_dir/module_name/config/ if (config_source[0] == '\0') { Dmod_SnPrintf(config_source, sizeof(config_source), "%s/%s/config/%s", - output_dir, module_name, config_path); + output_dir, module_name, substituted_config_path); DMOD_LOG_INFO("Using default config location: %s\n", config_source); } @@ -916,17 +1063,17 @@ static bool CopyConfigurationFile(const char* module_name, const char* config_pa // Build full destination path char config_dest[DMOD_MAX_PATH_LEN]; - if (custom_dest_name && custom_dest_name[0] != '\0') { + if (final_dest_name && final_dest_name[0] != '\0') { // Use custom destination filename (no module subdirectory) - Dmod_SnPrintf(config_dest, sizeof(config_dest), "%s/%s", config_dest_dir, custom_dest_name); + Dmod_SnPrintf(config_dest, sizeof(config_dest), "%s/%s", config_dest_dir, final_dest_name); } else { // Default: use module_name/filename pattern - // Extract filename from config_path - const char* filename = strrchr(config_path, '/'); + // Extract filename from substituted_config_path + const char* filename = strrchr(substituted_config_path, '/'); if (filename) { filename++; // Skip the '/' } else { - filename = config_path; + filename = substituted_config_path; } // Create module subdirectory path @@ -1780,6 +1927,7 @@ static void PrintUsage(const char* app_name) { Dmod_Printf(" -m, --manifest Path or URL to manifest file\n"); Dmod_Printf(" -o, --output-dir Output directory for downloaded modules\n"); Dmod_Printf(" --config-dir Directory where configuration files should be copied\n"); + Dmod_Printf(" -D, --define Define variable for config path substitution\n"); Dmod_Printf(" -t, --tools-name Tools name for variable substitution\n"); Dmod_Printf(" -a, --arch-name Architecture name for variable substitution\n"); Dmod_Printf(" --cpu-name CPU name for variable substitution (e.g., stm32f746ngh6)\n"); @@ -1814,6 +1962,7 @@ static void PrintUsage(const char* app_name) { Dmod_Printf(" %s mymodule@>=1.0<=2.0 # Download version in range [1.0, 2.0]\n", app_name); Dmod_Printf(" %s -d deps.dmd # Download all modules from deps.dmd\n", app_name); Dmod_Printf(" %s -d deps.dmd --config-dir ./config # Download modules and copy configs to ./config\n", app_name); + Dmod_Printf(" %s -d deps.dmd --config-dir ./config -D BOARD=stm32f7 # Use variable substitution in config paths\n", app_name); Dmod_Printf(" %s -m http://... module # Use custom manifest\n", app_name); Dmod_Printf(" %s --type dmfc module # Prefer dmfc files\n", app_name); Dmod_Printf(" %s -a armv7-cortex-m7 module # Use arch name directly\n", app_name); @@ -2236,6 +2385,38 @@ int main(int argc, char* argv[]) { } config_dir = argv[i]; } + else if (strcmp(argv[i], "-D") == 0 || strcmp(argv[i], "--define") == 0) { + if (++i >= argc) { + DMOD_LOG_ERROR("Error: %s requires an argument\n", argv[i-1]); + return 1; + } + // Parse VAR=value format + char* equals = strchr(argv[i], '='); + if (!equals) { + DMOD_LOG_ERROR("Error: --define requires format VAR=value, got: %s\n", argv[i]); + return 1; + } + + // Extract variable name and value + size_t name_len = equals - argv[i]; + char var_name[128]; + if (name_len >= sizeof(var_name)) { + DMOD_LOG_ERROR("Error: Variable name too long: %s\n", argv[i]); + return 1; + } + + strncpy(var_name, argv[i], name_len); + var_name[name_len] = '\0'; + + const char* var_value = equals + 1; + + if (!AddUserVariable(var_name, var_value)) { + DMOD_LOG_ERROR("Error: Too many variables defined (max %d)\n", MAX_USER_VARIABLES); + return 1; + } + + DMOD_LOG_INFO("Defined variable: %s=%s\n", var_name, var_value); + } else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--tools-name") == 0) { if (++i >= argc) { DMOD_LOG_ERROR("Error: %s requires an argument\n", argv[i-1]);