diff --git a/docs/dmd-file-format.md b/docs/dmd-file-format.md index 99926961..78c8ca4b 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,85 @@ 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] [custom_dest_name]` + +**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. + +**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 +# Install modules and copy configuration files to ./config directory +dmf-get -d project-deps.dmd --config-dir ./config +``` + +**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: @@ -95,6 +181,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 diff --git a/tests/lib/tests_dmod_dependencies.cpp b/tests/lib/tests_dmod_dependencies.cpp index 2d11396f..08a9aff7 100644 --- a/tests/lib/tests_dmod_dependencies.cpp +++ b/tests/lib/tests_dmod_dependencies.cpp @@ -396,3 +396,182 @@ 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); +} + +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 d3b83878..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* 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"); @@ -86,6 +87,22 @@ 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'; + } else { + 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; @@ -187,7 +204,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,12 +242,18 @@ static bool ParseLine(Dmod_DependenciesContext_t* ctx, char* line) { line = TrimWhitespace(line); } + // 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 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); @@ -245,13 +268,97 @@ 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'; + // 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, version_start, version_len); + module_version[version_len] = '\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 { + // 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 (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 { - // No version + // Only module name strncpy(module_name, line, sizeof(module_name) - 1); module_name[sizeof(module_name) - 1] = '\0'; @@ -270,7 +377,9 @@ 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, + 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 ff750fde..2e958249 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,8 @@ 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) */ + 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 d7a13f16..3610071b 100644 --- a/tools/lib/dmod_dependencies/vscode-dmd/example.dmd +++ b/tools/lib/dmod_dependencies/vscode-dmd/example.dmd @@ -11,6 +11,29 @@ spi@1.0 i2c@2.5 uart@1.2.3 +# Module entries with configuration files +# 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 + +# 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 +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 @@ -35,3 +58,9 @@ 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 + +# 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/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 diff --git a/tools/system/dmf-get/main.c b/tools/system/dmf-get/main.c index 482c6645..2f453069 100644 --- a/tools/system/dmf-get/main.c +++ b/tools/system/dmf-get/main.c @@ -43,6 +43,139 @@ #define CACHE_MANIFESTS_SUBDIR "manifests" #define CACHE_PACKAGES_SUBDIR "packages" +// Buffer sizes +#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; @@ -839,6 +972,176 @@ 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 + * @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* 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; + } + + // 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]; + Dmod_SnPrintf(dmr_path, sizeof(dmr_path), "%s/%s.dmr", output_dir, module_name); + + 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) { + 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 + substituted config_path + Dmod_SnPrintf(config_source, sizeof(config_source), "%s/%s", + res_entry.destination, substituted_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, substituted_config_path); + DMOD_LOG_INFO("Using default config location: %s\n", config_source); + } + + // 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; + } + + // 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; + } + + // Build full destination path + char config_dest[DMOD_MAX_PATH_LEN]; + + 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, final_dest_name); + } else { + // Default: use module_name/filename pattern + // Extract filename from substituted_config_path + const char* filename = strrchr(substituted_config_path, '/'); + if (filename) { + filename++; // Skip the '/' + } else { + filename = substituted_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); + } + + // 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 + 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); + 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 +1926,8 @@ 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(" -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"); @@ -1656,6 +1961,8 @@ 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 -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); @@ -2024,6 +2331,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 +2378,45 @@ 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], "-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]); @@ -2482,6 +2829,13 @@ 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); + 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"); + } } }