From 19f5073e0d64155f5885de5130a838c888dd4028 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Mon, 12 Oct 2026 12:38:40 +0200 Subject: [PATCH 01/12] [processing] Find the processing headers with any path separator lbuild v1.23 returns relative paths with forward slashes on all hosts, so counting the native separator found no header on Windows and modm/processing.hpp stayed empty. --- src/modm/processing/module.lb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modm/processing/module.lb b/src/modm/processing/module.lb index 1fd866efd6..8f75c62b49 100644 --- a/src/modm/processing/module.lb +++ b/src/modm/processing/module.lb @@ -11,7 +11,6 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. # ----------------------------------------------------------------------------- -import os def init(module): module.name = ":processing" @@ -36,5 +35,5 @@ def build(env): env.template("fiber.hpp.in") env.outbasepath = "modm/src/modm" - headers = env.generated_local_files(filterfunc=lambda path: ".hpp" in path and path.count(os.path.sep) == 1) + headers = env.generated_local_files(filterfunc=lambda path: ".hpp" in path and path.replace("\\", "/").count("/") == 1) env.template("processing.hpp.in", substitutions={"headers": sorted(headers)}) From 06dca3a34ed5a6d518fbd1cd9dfaf9d5199bd8d3 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Mon, 12 Oct 2026 23:12:21 +0200 Subject: [PATCH 02/12] [build] Require lbuild v1.23.0 It returns all relative paths with forward slashes, also on Windows. --- repo.lb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo.lb b/repo.lb index bfce277f0c..740c7fc16f 100644 --- a/repo.lb +++ b/repo.lb @@ -27,7 +27,7 @@ def StrictVersion(v): # Check for miminum required lbuild version import lbuild -min_lbuild_version = "1.21.6" +min_lbuild_version = "1.23.0" if StrictVersion(getattr(lbuild, "__version__", "0.1.0")) < StrictVersion(min_lbuild_version): print("modm requires at least lbuild v{}, please upgrade!\n" " pip3 install -U lbuild".format(min_lbuild_version)) From c3ce508fc56c63464301b9686ae4acc3877c8bd2 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Sun, 11 Oct 2026 14:14:31 +0200 Subject: [PATCH 03/12] [build] Use a response file for long archive commands Archiving many object files exceeds the command line length on Windows. SCons only writes the file when the command is too long. It uses forward slashes, since ar reads a backslash in a response file as an escape. --- tools/build_script_generator/scons/site_tools/gcc_retarget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/build_script_generator/scons/site_tools/gcc_retarget.py b/tools/build_script_generator/scons/site_tools/gcc_retarget.py index b947d4f245..43b9046da8 100644 --- a/tools/build_script_generator/scons/site_tools/gcc_retarget.py +++ b/tools/build_script_generator/scons/site_tools/gcc_retarget.py @@ -62,6 +62,9 @@ def generate(env, **kw): raise RuntimeError(f'Unsupported compiler: "{compiler}"') env['AR'] = prefix + 'ar' + # Many object files exceed the command line length on Windows. The response + # file needs forward slashes, since ar reads a backslash as an escape there. + env['ARCOM'] = "${TEMPFILE('$AR $ARFLAGS $TARGET.posix $SOURCES.posix','$ARCOMSTR')}" env['RANLIB'] = prefix + 'ranlib' env['AS'] = prefix + 'as' if suffix == '' else prefix + 'gcc' + suffix From 8a40de9ef17ce65477a862917d64f97d2cabf373 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Sun, 11 Oct 2026 14:14:31 +0200 Subject: [PATCH 04/12] [ext] Do not compile the LVGL Helium assembly It is only for Cortex-M55 and SCons does not preprocess .S files on Windows, so its include guard has no effect there. --- ext/lvgl/lvgl.lb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/lvgl/lvgl.lb b/ext/lvgl/lvgl.lb index c278561d08..b62da43e09 100644 --- a/ext/lvgl/lvgl.lb +++ b/ext/lvgl/lvgl.lb @@ -88,7 +88,8 @@ def build(env): env.copy("lvgl/lvgl_private.h") env.copy("lvgl/include") env.template("lv_conf.h.in") - files = env.copy("lvgl/src") + # The Helium assembly is for Cortex-M55 only and not preprocessed on Windows + files = env.copy("lvgl/src", ignore=env.ignore_files("*.S")) env.collect(":build:ccflags", "-Wno-maybe-uninitialized", operations=files) env.substitutions = {"has_debug": env.has_module(":debug")} env.template("lv_modm_port.cpp.in") From 5058ea5df4e192da11ce96c55985c46b0a410eab Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Sun, 11 Oct 2026 18:44:23 +0200 Subject: [PATCH 05/12] [build] Use a response file for long link commands An application with about a hundred source files exceeds the 8191 characters that cmd.exe accepts when linking with SCons on Windows. The linker reads a backslash in a response file as an escape character. The target and sources are written with forward slashes, but SCons has no such form for the library paths, libraries and link flags, which contain absolute Windows paths. Their backslashes are escaped instead, only for the link command of the program. --- .../scons/resources/build_target.py.in | 7 ++++++- .../scons/site_tools/gcc_retarget.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/build_script_generator/scons/resources/build_target.py.in b/tools/build_script_generator/scons/resources/build_target.py.in index 2372691704..e13b25fdc7 100644 --- a/tools/build_script_generator/scons/resources/build_target.py.in +++ b/tools/build_script_generator/scons/resources/build_target.py.in @@ -11,10 +11,15 @@ # ----------------------------------------------------------------------------- from os.path import join, abspath, relpath +from SCons.Subst import quote_spaces def build_target(env, sources): # Building application - program = env.Program(target=env["CONFIG_PROJECT_NAME"]+"{{ program_extension }}", source=sources) + # The linker reads a backslash in a response file as an escape character. + # SCons only provides the target and sources with forward slashes, but not + # the library paths, libraries and link flags, which are escaped instead. + program = env.Program(target=env["CONFIG_PROJECT_NAME"]+"{{ program_extension }}", source=sources, + TEMPFILEARGESCFUNC=lambda arg: quote_spaces(arg.replace("\\", "\\\\"))) chosen_program = env.ChooseFirmware(program) # Clean additional artifacts env.Clean(program, join(env["BUILDPATH"], env["CONFIG_PROJECT_NAME"]+".bin")) diff --git a/tools/build_script_generator/scons/site_tools/gcc_retarget.py b/tools/build_script_generator/scons/site_tools/gcc_retarget.py index 43b9046da8..95b78803a4 100644 --- a/tools/build_script_generator/scons/site_tools/gcc_retarget.py +++ b/tools/build_script_generator/scons/site_tools/gcc_retarget.py @@ -89,6 +89,8 @@ def generate(env, **kw): env['STRIP'] = prefix + 'strip' env['LINK'] = env['CXX'] + # Many application object files exceed the command line length on Windows. + env['LINKCOM'] = "${TEMPFILE('$LINK -o $TARGET.posix $LINKFLAGS $__RPATH $SOURCES.posix $_LIBDIRFLAGS $_LIBFLAGS','$LINKCOMSTR')}" builder_hex = Builder( action=Action("$OBJCOPY -O ihex $SOURCE $TARGET", From 3abbedeeeb28f3c4f2c4bea58e3d6dea783a1af8 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Mon, 12 Oct 2026 17:16:42 +0200 Subject: [PATCH 06/12] [build] Remove the path filters for Windows Paths were written with backslashes on Windows and every use in a template had to opt into escaping them with the modm.windowsify filter. Wherever that was forgotten, a path like generated\nanopb or generated\xpcc turned into an escape sequence or a syntax error. lbuild now returns all relative paths with forward slashes, which all tools accept on Windows as well. This makes the modm.windowsify and modm.posixify filters and their escape levels unnecessary. --- repo.lb | 13 ------------- src/modm/architecture/interface.hpp.in | 2 +- src/modm/math/math.hpp.in | 2 +- src/modm/platform/platform.hpp.in | 2 +- src/modm/processing/processing.hpp.in | 2 +- .../cmake/resources/CMakeLists.txt.in | 10 +++++----- .../cmake/resources/ModmConfiguration.cmake.in | 2 +- .../resources/builder_compilation_db.py.in | 2 +- .../compilation_db/resources/compilation_db.py.in | 10 +++++----- tools/build_script_generator/openocd.cfg.in | 6 +++--- tools/build_script_generator/scons/module.lb | 5 ++--- .../scons/resources/SConscript.in | 14 +++++++------- .../scons/resources/SConstruct.in | 6 +++--- tools/ide/vscode/c_cpp_properties.json.in | 2 +- 14 files changed, 32 insertions(+), 46 deletions(-) diff --git a/repo.lb b/repo.lb index 740c7fc16f..6407087d28 100644 --- a/repo.lb +++ b/repo.lb @@ -193,19 +193,6 @@ def init(repo): config, host if host in config else "windows")) # Add Jinja2 filters commonly used in this repository - if 'Windows' in platform.platform(): - def windowsify(path, escape_level): - escaped = "\\" * (2 ** escape_level) - return path.replace("\\", escaped) - def posixify(path): - return path.replace("\\", "/") - else: - def windowsify(path, escape_level): - return path - def posixify(path): - return path - repo.add_filter("modm.windowsify", windowsify) - repo.add_filter("modm.posixify", posixify) repo.add_filter("modm.ord", lambda letter: ord(letter[0].lower()) - ord("a")) repo.add_filter("modm.chr", lambda num: chr(num + ord("A"))) repo.add_filter("modm.digsep", lambda num: f"{num:,}".replace(",", "'")) diff --git a/src/modm/architecture/interface.hpp.in b/src/modm/architecture/interface.hpp.in index e46517f01d..e00cf61b2b 100644 --- a/src/modm/architecture/interface.hpp.in +++ b/src/modm/architecture/interface.hpp.in @@ -15,7 +15,7 @@ #include %% for header in headers -#include "{{ header | modm.posixify }}" +#include "{{ header }}" %% endfor #include "interface/peripheral.hpp" diff --git a/src/modm/math/math.hpp.in b/src/modm/math/math.hpp.in index 176a4ac308..3d1347d1c9 100644 --- a/src/modm/math/math.hpp.in +++ b/src/modm/math/math.hpp.in @@ -13,7 +13,7 @@ #define MODM_MATH_HPP %% for header in headers -#include "{{ header | modm.posixify }}" +#include "{{ header }}" %% endfor #endif // MODM_MATH_HPP diff --git a/src/modm/platform/platform.hpp.in b/src/modm/platform/platform.hpp.in index 2c15fe7441..5859028c58 100644 --- a/src/modm/platform/platform.hpp.in +++ b/src/modm/platform/platform.hpp.in @@ -13,5 +13,5 @@ #include %% for header in headers -#include "{{ header | modm.posixify }}" +#include "{{ header }}" %% endfor diff --git a/src/modm/processing/processing.hpp.in b/src/modm/processing/processing.hpp.in index b180eb7ff1..dce1c6406b 100644 --- a/src/modm/processing/processing.hpp.in +++ b/src/modm/processing/processing.hpp.in @@ -15,7 +15,7 @@ #define MODM_PROCESSING_HPP %% for header in headers -#include "{{ header | modm.posixify }}" +#include "{{ header }}" %% endfor #endif // MODM_PROCESSING_HPP diff --git a/tools/build_script_generator/cmake/resources/CMakeLists.txt.in b/tools/build_script_generator/cmake/resources/CMakeLists.txt.in index 61fb1de034..1f990ba092 100644 --- a/tools/build_script_generator/cmake/resources/CMakeLists.txt.in +++ b/tools/build_script_generator/cmake/resources/CMakeLists.txt.in @@ -17,7 +17,7 @@ include(cmake/ModmConfiguration.cmake) %% if asm_sources | length set_source_files_properties( %% for file in asm_sources | sort - {{ file | relocate | modm.posixify }} + {{ file | relocate }} %% endfor PROPERTIES LANGUAGE CXX ) @@ -27,7 +27,7 @@ set_source_files_properties( %% if sources | length add_library({{ repo }} OBJECT %% for file, flags in sources | sort - {{ file | relocate | modm.posixify }} + {{ file | relocate }} %% endfor ) %% else @@ -36,7 +36,7 @@ add_library({{ repo }} INTERFACE ) %% if per_file_attr | length %% for section in per_file_attr | sort -{{ section | modm.posixify }} +{{ section }} %% endfor %% endif @@ -57,12 +57,12 @@ target_include_directories({{ repo }} SYSTEM INTERFACE %% endif %% for path in include_paths | sort - {{ path | relocate | modm.posixify }} + {{ path | relocate }} %% endfor %% if sources | length PRIVATE %% for path in private_include_paths | sort - {{ path | relocate | modm.posixify }} + {{ path | relocate }} %% endfor %% endif ) diff --git a/tools/build_script_generator/cmake/resources/ModmConfiguration.cmake.in b/tools/build_script_generator/cmake/resources/ModmConfiguration.cmake.in index d44e3442a2..10097ec51e 100644 --- a/tools/build_script_generator/cmake/resources/ModmConfiguration.cmake.in +++ b/tools/build_script_generator/cmake/resources/ModmConfiguration.cmake.in @@ -99,7 +99,7 @@ function(modm_target_config_create target target_arch target_options target_warn %% macro generate_flags_for_profile(name, profile) set({{ name | upper }}{{ "_" ~ (profile | upper) if profile | length else "" }} %% for flag in flags[name][profile] | sort - {{ flag | flags_format | modm.posixify }} + {{ flag | flags_format }} %% endfor ) %% endmacro diff --git a/tools/build_script_generator/compilation_db/resources/builder_compilation_db.py.in b/tools/build_script_generator/compilation_db/resources/builder_compilation_db.py.in index 5cb2086f47..86cf9686c8 100644 --- a/tools/build_script_generator/compilation_db/resources/builder_compilation_db.py.in +++ b/tools/build_script_generator/compilation_db/resources/builder_compilation_db.py.in @@ -13,7 +13,7 @@ from pathlib import Path env = defaultdict(list) env["CONFIG_PROJECT_NAME"] = "{{ options[":build:project.name"] }}" -env["CONFIG_BUILD_BASE"] = os.path.abspath("{{ build_path | modm.windowsify(escape_level=1)}}") +env["CONFIG_BUILD_BASE"] = os.path.abspath("{{ build_path }}") env["CONFIG_BUILD_PROFILE"] = "debug" if "--debug" in sys.argv else "release" env["BUILDPATH"] = os.path.join(env["CONFIG_BUILD_BASE"], env["CONFIG_BUILD_PROFILE"]) diff --git a/tools/build_script_generator/compilation_db/resources/compilation_db.py.in b/tools/build_script_generator/compilation_db/resources/compilation_db.py.in index 178e8fc1a7..5ae31d115c 100644 --- a/tools/build_script_generator/compilation_db/resources/compilation_db.py.in +++ b/tools/build_script_generator/compilation_db/resources/compilation_db.py.in @@ -103,13 +103,13 @@ if profile == "{{ profile }}": env["CPPPATH"].extend([ %% for path in include_paths | sort - abspath(r"{{ path | modm.windowsify(escape_level=0) }}"), + abspath(r"{{ path }}"), %% endfor ]) env["FILES"].extend([ %% for file, flags in sources if not flags | length - (r"{{ file | modm.windowsify(escape_level=0) }}", {}), + (r"{{ file }}", {}), %% endfor ]) %% for file, flags in sources @@ -120,14 +120,14 @@ flags = { {%- for key, profiles in flags.items() if "" in profiles %}"{{ key | u if profile == "{{profile}}": flags["{{ key | upper }}"].extend({{flags}}); %% endfor %% endfor -env["FILES"].append( (r"{{ file | modm.windowsify(escape_level=0) }}", flags) ) +env["FILES"].append( (r"{{ file }}", flags) ) %% endif %% endfor env["STATIC_LIBS"].append( ("{{repo}}/lib{{repo}}.a", [ %% for file, flags in sources - r"{{ file | modm.windowsify(escape_level=0) }}", + r"{{ file }}", %% endfor ]) ) @@ -141,6 +141,6 @@ env["LIBS"].extend([ env["LIBPATH"].extend([ abspath(join(env["BUILDPATH"], "{{repo}}")), %% for library in library_paths | sort - abspath(r"{{ library | modm.windowsify(escape_level=0) }}"), + abspath(r"{{ library }}"), %% endfor ]) diff --git a/tools/build_script_generator/openocd.cfg.in b/tools/build_script_generator/openocd.cfg.in index 3c84a5b470..3338e5b5ae 100644 --- a/tools/build_script_generator/openocd.cfg.in +++ b/tools/build_script_generator/openocd.cfg.in @@ -1,12 +1,12 @@ %% for path in openocd_search_dirs | sort -add_script_search_dir {{ path | modm.windowsify(escape_level=1) }} +add_script_search_dir {{ path }} %% endfor %# Include the users config before the modm config %% if openocd_user_path is defined -source [find {{ openocd_user_path | modm.windowsify(escape_level=1) }}] +source [find {{ openocd_user_path }}] %% endif %% for file in openocd_sources | sort -source [find {{ file | modm.windowsify(escape_level=1) }}] +source [find {{ file }}] %% endfor proc modm_itm_log { OUTPUT F_CPU {BAUDRATE 2000000} } { diff --git a/tools/build_script_generator/scons/module.lb b/tools/build_script_generator/scons/module.lb index d29e228516..5337bd4974 100644 --- a/tools/build_script_generator/scons/module.lb +++ b/tools/build_script_generator/scons/module.lb @@ -153,7 +153,7 @@ def post_build(env): has_nanopb = env.has_module(":nanopb") repositories = [p for p in env.buildlog.repositories if isdir(env.real_outpath(p, basepath="."))] repositories.sort(key=lambda name: "0" if name == "modm" else name) - generated_paths = [join(env.relcwdoutpath(""), r) for r in repositories] + generated_paths = [env.relcwdoutpath("") + "/" + r for r in repositories] subs = env.query("::device") cache_dir = env["cache_dir"] @@ -192,8 +192,7 @@ def post_build(env): }) if has_nanopb: subs["nanopb_source"] = [env.relcwdoutpath(p) for p in env.get(":nanopb:sources", [])] - # forward slashes, since a Windows path like generated\nanopb contains an escape sequence - subs["nanopb_path"] = env.relcwdoutpath(env.get(":nanopb:path", ".")).replace("\\", "/") + subs["nanopb_path"] = env.relcwdoutpath(env.get(":nanopb:path", ".")) if subs["platform"] == "avr": subs.update(env.query("::avrdude_options")) if subs["platform"] == "sam": diff --git a/tools/build_script_generator/scons/resources/SConscript.in b/tools/build_script_generator/scons/resources/SConscript.in index afd07a3349..ca5aa7a443 100644 --- a/tools/build_script_generator/scons/resources/SConscript.in +++ b/tools/build_script_generator/scons/resources/SConscript.in @@ -33,7 +33,7 @@ env["COMPILER"] = "{{ options.get(":build:compiler", "gcc") }}" # SCons tools env.Append(toolpath=[ %% for toolpath in toolpaths | sort - abspath("{{ toolpath | modm.windowsify(escape_level=1) }}"), + abspath("{{ toolpath }}"), %% endfor ]) %% endif @@ -44,7 +44,7 @@ env.Tool("{{tool}}") %% macro generate_flags_for_profile(name, profile, append=False) env{% if append %}.Append({{name | upper}}{% else %}["{{name | upper}}"]{% endif %} = [ %% for flag in flags[name][profile] | sort - {{ flag | flags_format | modm.windowsify(escape_level=1) }}, + {{ flag | flags_format }}, %% endfor ]{% if append %}){% endif -%} %% endmacro @@ -157,7 +157,7 @@ env.AppendUnique(CPPFLAGS=[ env.AppendUnique(CPPPATH=[ %% for path in (include_paths + system_include_paths) | sort - abspath("{{ path | relocate | modm.windowsify(escape_level=1) }}"), + abspath("{{ path | relocate }}"), %% endfor ]) %% if system_include_paths | length @@ -165,14 +165,14 @@ env.AppendUnique(CPPPATH=[ # compiler ignores -I for paths that are also passed as -isystem. env.AppendUnique(CPPFLAGS=[ %% for path in system_include_paths | sort - "-isystem" + abspath("{{ path | relocate | modm.windowsify(escape_level=1) }}"), + "-isystem" + abspath("{{ path | relocate }}"), %% endfor ]) %% endif files = [ %% for file, flags in sources if not flags | length - env.File("{{ file | relocate | modm.windowsify(escape_level=1) }}"), + env.File("{{ file | relocate }}"), %% endfor ] %% for file, flags in sources @@ -183,7 +183,7 @@ flags = { {%- for key, profiles in flags.items() if "" in profiles %}"{{ key | u if profile == "{{profile}}": flags["{{ key | upper }}"].extend({{flags}}); %% endfor %% endfor -files.append(env.Object("{{ file | relocate | modm.windowsify(escape_level=1) }}", **flags)) +files.append(env.Object("{{ file | relocate }}", **flags)) %% endif %% endfor @@ -201,7 +201,7 @@ env.AppendUnique(LIBPATH=[ %% endif abspath(str(library[0].get_dir())), %% for library in library_paths | sort - abspath("{{ library | relocate | modm.windowsify(escape_level=1) }}"), + abspath("{{ library | relocate }}"), %% endfor ]) %% if packages | length diff --git a/tools/build_script_generator/scons/resources/SConstruct.in b/tools/build_script_generator/scons/resources/SConstruct.in index 9b5aa2b5f1..0abf610ce1 100644 --- a/tools/build_script_generator/scons/resources/SConstruct.in +++ b/tools/build_script_generator/scons/resources/SConstruct.in @@ -25,12 +25,12 @@ import os from os.path import join, abspath %% if cache_dir | length -CacheDir(abspath("{{ cache_dir | modm.windowsify(escape_level=1)}}")) +CacheDir(abspath("{{ cache_dir }}")) %% endif env = DefaultEnvironment(tools=[], ENV=os.environ) env["CONFIG_PROJECT_NAME"] = "{{ options[":build:project.name"] }}" -env["CONFIG_BUILD_BASE"] = abspath("{{ build_path | modm.windowsify(escape_level=1)}}") -env["CONFIG_ARTIFACT_PATH"] = abspath("{{ artifact_path | modm.windowsify(escape_level=1)}}") +env["CONFIG_BUILD_BASE"] = abspath("{{ build_path }}") +env["CONFIG_ARTIFACT_PATH"] = abspath("{{ artifact_path }}") generated_paths = {{ generated_paths }} # Building all libraries diff --git a/tools/ide/vscode/c_cpp_properties.json.in b/tools/ide/vscode/c_cpp_properties.json.in index 8ae5a9a592..ef4d38ec36 100644 --- a/tools/ide/vscode/c_cpp_properties.json.in +++ b/tools/ide/vscode/c_cpp_properties.json.in @@ -5,7 +5,7 @@ "name": "{{ platform }}GCC ({{ profile | capitalize }})", "includePath": [ %% for path in include_paths | sort - "${workspaceFolder}/{{ path | modm.windowsify(escape_level=1) }}"{% if not loop.last%},{% endif %} + "${workspaceFolder}/{{ path }}"{% if not loop.last%},{% endif %} %% endfor ], "defines": [ From c2423a0bb84f7609ba9850965834ff57318a5f48 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Sun, 11 Oct 2026 18:44:56 +0200 Subject: [PATCH 07/12] [build] Call the CompilationDB builder without a shell All application sources are passed as arguments, which exceeds the 8191 characters that cmd.exe accepts for about a hundred files, and SCons did not even report that as an error. The interpreter is also only called python3 on Windows if it was installed by the GitHub setup-python action. --- .../scons/resources/build_target.py.in | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/build_script_generator/scons/resources/build_target.py.in b/tools/build_script_generator/scons/resources/build_target.py.in index e13b25fdc7..51c6dbd456 100644 --- a/tools/build_script_generator/scons/resources/build_target.py.in +++ b/tools/build_script_generator/scons/resources/build_target.py.in @@ -10,6 +10,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. # ----------------------------------------------------------------------------- +import subprocess, sys from os.path import join, abspath, relpath from SCons.Subst import quote_spaces @@ -28,9 +29,12 @@ def build_target(env, sources): env.Clean(program, join(env["BUILDPATH"], env["CONFIG_PROJECT_NAME"]+".uf2")) %% if with_compilation_db + # Called without a shell, since many sources exceed its command line length + # on Windows, where the interpreter is also not called python3. + compilation_db = [sys.executable, "modm/tools/builder_compilation_db.py", "--" + env["CONFIG_PROFILE"]] + compilation_db += [relpath(env.File(s).srcnode().abspath) for s in env.Flatten(sources)] env.Command("compile_commands.json", sources, - "python3 modm/tools/builder_compilation_db.py --{} {}".format( - env["CONFIG_PROFILE"], " ".join(relpath(env.File(s).srcnode().abspath) for s in env.Flatten(sources)))) + env.Action(lambda target, source, env: subprocess.call(compilation_db), None)) env.Alias("compilation_db", "compile_commands.json") %% endif From 72dc58437edb109d5d5dbf3dc4ca77b6ee852419 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Sun, 11 Oct 2026 18:44:56 +0200 Subject: [PATCH 08/12] [build] Do not create bin and hex files for hosted CMake targets A 64-bit Windows executable is loaded at an address that does not fit into an Intel Hex file, so objcopy failed every hosted CMake build. --- .../cmake/resources/ModmConfiguration.cmake.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/build_script_generator/cmake/resources/ModmConfiguration.cmake.in b/tools/build_script_generator/cmake/resources/ModmConfiguration.cmake.in index 10097ec51e..050fab5896 100644 --- a/tools/build_script_generator/cmake/resources/ModmConfiguration.cmake.in +++ b/tools/build_script_generator/cmake/resources/ModmConfiguration.cmake.in @@ -169,10 +169,12 @@ function(modm_targets_create project_name) set_target_properties(${project_name} PROPERTIES SUFFIX ".elf") +%% if platform != "hosted" add_custom_command(TARGET ${project_name} POST_BUILD COMMAND ${CMAKE_OBJCOPY} -Obinary ${project_name}.elf ${project_name}.bin COMMAND ${CMAKE_OBJCOPY} -Oihex ${project_name}.elf ${project_name}.hex) +%% endif %% if core.startswith("cortex-m") add_custom_command(TARGET ${project_name} From fbca9c18524291c08ad0f15fdc4b2529bbc2926a Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Sun, 11 Oct 2026 18:44:56 +0200 Subject: [PATCH 09/12] [tools] Find the XPCC DTD next to an absolute Windows path lxml passes the DTD of an XML file with an absolute Windows path as a file URL, which was not recognized as an existing file, so the DTD was searched for in the wrong folder. Together with the forward slashes in the SConstruct this lets the XPCC examples compile on Windows again. --- examples/avr/xpcc/receiver/project.xml | 1 - examples/avr/xpcc/sender/project.xml | 1 - tools/xpcc_generator/xmlparser/parser.py | 5 ++++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/avr/xpcc/receiver/project.xml b/examples/avr/xpcc/receiver/project.xml index 6cf803d986..8247ec92d7 100644 --- a/examples/avr/xpcc/receiver/project.xml +++ b/examples/avr/xpcc/receiver/project.xml @@ -1,5 +1,4 @@ - diff --git a/examples/avr/xpcc/sender/project.xml b/examples/avr/xpcc/sender/project.xml index 541f5698bc..e79e186b86 100644 --- a/examples/avr/xpcc/sender/project.xml +++ b/examples/avr/xpcc/sender/project.xml @@ -1,5 +1,4 @@ - diff --git a/tools/xpcc_generator/xmlparser/parser.py b/tools/xpcc_generator/xmlparser/parser.py index 9a2829b5f6..d928160a98 100644 --- a/tools/xpcc_generator/xmlparser/parser.py +++ b/tools/xpcc_generator/xmlparser/parser.py @@ -22,6 +22,8 @@ import os import sys import logging +from urllib.parse import urlparse +from urllib.request import url2pathname from .parser_exception import ParserException from . import utils @@ -75,7 +77,8 @@ def __init__(self, dtdPath): self.dtdPath = dtdPath def resolve(self, url, id, context): - if os.path.isfile(url): + # The DTD next to an absolute Windows path is passed as a file URL + if os.path.isfile(url) or os.path.isfile(url2pathname(urlparse(url).path)): return None else: filename = os.path.basename(url) From 81431d3911fa0281ec7983b2d3b705d4e369f582 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Sun, 11 Oct 2026 20:14:38 +0200 Subject: [PATCH 10/12] [tools] Compile CMake examples on Windows The CMake generator was never tested on Windows. The commands were written for a POSIX shell, they now only use CMake itself. MinGW Makefiles are generated on Windows, since they run their commands with cmd.exe and need nothing but the make that comes with MinGW. --- tools/scripts/examples_compile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/scripts/examples_compile.py b/tools/scripts/examples_compile.py index e343a911cc..523ea79d6e 100755 --- a/tools/scripts/examples_compile.py +++ b/tools/scripts/examples_compile.py @@ -68,10 +68,10 @@ def build(project): commands.append( ("scons compilation_db", "CompilationDB (via SCons)") ) if ":build:make" in project_cfg and not is_running_on_windows: commands.append( ("make build", "Make") ) - elif ":build:cmake" in project_cfg and not is_running_on_windows: + elif ":build:cmake" in project_cfg: build_dir = re.search(r'name=".+?:build:build.path">(.*?)', project_cfg)[1] - cmd = "cmake -E make_directory {}/cmake-build-release; ".format(build_dir) - cmd += '(cd {}/cmake-build-release && cmake -DCMAKE_BUILD_TYPE=MinSizeRel -G "Unix Makefiles" {}); '.format(build_dir, path.absolute()) + generator = "MinGW Makefiles" if is_running_on_windows else "Unix Makefiles" + cmd = 'cmake -S . -B {}/cmake-build-release -DCMAKE_BUILD_TYPE=MinSizeRel -G "{}" && '.format(build_dir, generator) cmd += "cmake --build {}/cmake-build-release".format(build_dir) commands.append( (cmd, "CMake") ) From ca79c3dde6a87787bf43f960dd17e8d94205e2c3 Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Sun, 11 Oct 2026 23:42:18 +0200 Subject: [PATCH 11/12] [ci] Select Windows examples by what can break there The STM32 examples were whole board folders, so Windows bugs in the build system were only found if an affected example happened to be in one of them. The list now names examples with preprocessed assembly, generated paths, many object files, CMake and CompilationDB. The Nanopb generator needs protobuf and grpcio-tools. --- .github/workflows/windows.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index fcabd1089a..524d3059ad 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -93,7 +93,7 @@ jobs: - name: Install Python packages if: always() run: | - pip install --upgrade --upgrade-strategy=eager modm scons future + pip install --upgrade --upgrade-strategy=eager modm scons future protobuf grpcio-tools - name: Show Version Information if: always() @@ -101,6 +101,7 @@ jobs: gcc --version g++ --version make --version + cmake --version arm-none-eabi-g++ --version avr-g++ --version lbuild --version @@ -115,6 +116,7 @@ jobs: - name: Hosted Examples if: always() shell: bash + # Compiled and run, build and git information is generated while building. run: | (cd examples && python ../tools/scripts/examples_compile.py linux/assert linux/block_device/ram linux/build_info linux/git linux/logger linux/printf linux/etl linux/fiber) @@ -127,13 +129,25 @@ jobs: - name: Compile STM32 Examples if: always() shell: bash + # These examples are selected by what can break on Windows. run: | - (cd examples && python ../tools/scripts/examples_compile.py nucleo_f031k6 nucleo_f103rb nucleo_f303re nucleo_f411re nucleo_f746zg) - (cd examples && python ../tools/scripts/examples_compile.py nucleo_g071rb nucleo_l031k6 nucleo_l152re nucleo_l432kc nucleo_g474re) + # Preprocessed assembly, which SCons only recognizes as `.sx` there: + # RTT, CrashCatcher and LVGL without its Helium assembly. Fibers + # only use inline assembly. + (cd examples && python ../tools/scripts/examples_compile.py blue_pill_f103/flash stm32f469_discovery/hard_fault nucleo_h563zi/fiber nucleo_l452re/lvgl) + # Paths in generated build scripts, where a backslash is an escape + # character: Nanopb and image sources. See AVR for XPCC. + (cd examples && python ../tools/scripts/examples_compile.py nucleo_f429zi/nanopb stm32_f4ve/gui) + # Many object files: LVGL, CMSIS-DSP and Ethernet exceed the command + # line length of cmd.exe when archiving, FreeRTOS and TinyUSB do not. + (cd examples && python ../tools/scripts/examples_compile.py stm32f469_discovery/lvgl nucleo_f429zi/ethernet nucleo_f429zi/freertos_static nucleo_f429zi/usb_freertos) + # CMake and CompilationDB generators + (cd examples && python ../tools/scripts/examples_compile.py stm32f469_discovery/assert nucleo_f429zi/cmsis_dsp/convolution stm32_f4ve/flash) - name: Compile AVR Examples if: always() shell: bash + # Includes the XPCC generator, image sources and CMake. run: | (cd examples && ../tools/scripts/examples_compile.py avr arduino_nano arduino_uno srxe) From 7d99e46f3bc8c61f708ea2593f21fde23449d6cb Mon Sep 17 00:00:00 2001 From: Niklas Hauser Date: Mon, 12 Oct 2026 09:11:05 +0200 Subject: [PATCH 12/12] [tools] Use an integer as fallback for the Git timestamps Outside of a Git repository or without Git installed the two timestamps fell back to the string "Unknown", which was generated as a char pointer and conflicted with their uint32_t declaration, so info_git.c did not compile. --- tools/modm_tools/info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/modm_tools/info.py b/tools/modm_tools/info.py index 1185c3ed80..eb18344d84 100644 --- a/tools/modm_tools/info.py +++ b/tools/modm_tools/info.py @@ -138,11 +138,11 @@ def git_info(directory, with_status=False): "MODM_GIT_AUTHOR": "Unknown", "MODM_GIT_AUTHOR_EMAIL": "Unknown", "MODM_GIT_AUTHOR_DATE": "Unknown", - "MODM_GIT_AUTHOR_DATE_TIMESTAMP": "Unknown", + "MODM_GIT_AUTHOR_DATE_TIMESTAMP": 0, "MODM_GIT_COMMITTER": "Unknown", "MODM_GIT_COMMITTER_EMAIL": "Unknown", "MODM_GIT_COMMITTER_DATE": "Unknown", - "MODM_GIT_COMMITTER_DATE_TIMESTAMP": "Unknown", + "MODM_GIT_COMMITTER_DATE_TIMESTAMP": 0, "MODM_GIT_SUBJECT": "Unknown", "MODM_GIT_CONFIG_USER_NAME": "Unknown", "MODM_GIT_CONFIG_USER_EMAIL": "Unknown",